@anthusai/papyrus 1.0.0-next.4 → 1.0.0-next.40
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/amplify/auth/redirect-urls.js +54 -0
- package/amplify/auth/redirect-urls.ts +66 -0
- package/amplify/auth/resource.js +20 -5
- package/amplify/auth/resource.ts +27 -5
- package/amplify/data/resource.js +28 -70
- package/amplify/data/resource.ts +29 -70
- package/amplify/data/schema.js +33 -58
- package/amplify/data/schema.ts +34 -58
- package/amplify/functions/category-action/handler.ts +5 -3
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +0 -10
- package/amplify/functions/console-chat-responder/resource.js +0 -7
- package/amplify/functions/console-chat-responder/resource.ts +0 -8
- package/amplify/functions/console-chat-responder/src/main.rs +0 -9
- package/amplify/functions/content-actions/handler.py +45 -3
- package/amplify/functions/shared/reference-curation-metadata.ts +29 -0
- package/amplify/inbound-email/ses-intake.js +86 -0
- package/amplify/inbound-email/ses-intake.ts +110 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/resolver-waves.js +26 -0
- package/amplify/resolver-waves.ts +34 -0
- package/amplify/site-backend-config.ts +36 -1
- package/amplify/site-backend-names.js +103 -0
- package/amplify/site-backend-names.ts +166 -0
- package/amplify/site-backend.js +93 -170
- package/amplify/site-backend.ts +126 -196
- package/amplify/storage/resource.js +3 -0
- package/amplify/storage/resource.ts +3 -0
- package/amplify/storage-backups/construct.js +31 -0
- package/amplify/storage-backups/construct.ts +47 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +2 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +2 -0
- package/app/api/media/[...path]/route.ts +5 -16
- package/app/api/revalidate/route.ts +8 -7
- package/app/articles/[slug]/page.tsx +6 -4
- package/app/blog-rhythm.css +278 -0
- package/app/globals.css +21 -0
- package/app/layout.tsx +16 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +6 -5
- package/app/newsroom/[section]/page.tsx +4 -3
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/layout.tsx +4 -9
- package/app/page.tsx +18 -5
- package/app/robots.ts +2 -5
- package/bin/papyrus-app.mjs +12 -2
- package/bin/papyrus-infra.mjs +29 -3
- package/components/amplify-client-provider.tsx +4 -22
- package/components/archive-shell.tsx +2 -22
- package/components/article-video.tsx +247 -3
- package/components/blog-page-background.tsx +23 -6
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-taxonomy-client.ts +51 -0
- package/components/newspaper.tsx +6 -2
- package/components/newsroom-access-gate.tsx +67 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-client-shell.tsx +12 -0
- package/components/newsroom-ops-overview.tsx +28 -11
- package/components/newsroom-ops-shell.tsx +3 -1
- package/components/newsroom-references-view.tsx +83 -8
- package/components/papyrus-console-shell.tsx +4 -2
- package/components/pictogram-figure.tsx +9 -2
- package/components/presentation-header.tsx +135 -0
- package/components/presentation-rhythm-hrule.tsx +17 -0
- package/components/reference-cyclotron-status.tsx +109 -0
- package/components/reference-relevance-review.tsx +75 -0
- package/components/settings-page.tsx +2 -1
- package/components/topic-steering-workspace.tsx +54 -64
- package/components/use-blog-header-obstacles.ts +81 -0
- package/components/use-brand-fonts-ready.ts +28 -0
- package/components/use-rhythm-overlay.ts +29 -0
- package/infra/amplify-app-shell.js +212 -50
- package/infra/amplify-app-shell.ts +240 -53
- package/infra/build-specs.js +14 -5
- package/infra/build-specs.ts +17 -7
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +1 -0
- package/infra/site-config.js +167 -10
- package/infra/site-config.ts +196 -14
- package/lib/amplify-outputs-path.ts +5 -1
- package/lib/blog-feature-solver.ts +459 -0
- package/lib/blog-rhythm.ts +134 -0
- package/lib/brand-fonts-watch.ts +43 -0
- package/lib/brand-fonts.ts +112 -0
- package/lib/cached-content-repository.ts +12 -0
- package/lib/category-repository.ts +42 -93
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-repository.ts +3 -0
- package/lib/content-source-context.ts +12 -2
- package/lib/content-types.ts +4 -0
- package/lib/edition-routes.ts +31 -29
- package/lib/graphql-content-repository.ts +61 -9
- package/lib/media-route-response.ts +45 -0
- package/lib/newspaper-layout.ts +3 -2
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-base-path.ts +12 -3
- package/lib/newsroom-category-drilldown.ts +3 -2
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-forum-routes.ts +6 -4
- package/lib/newsroom-index-filters.ts +14 -10
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +3 -0
- package/lib/oauth-redirect-priority.ts +40 -0
- package/lib/papyrus-web-locations.ts +7 -6
- package/lib/presentation-footer.ts +3 -2
- package/lib/pretext-layout.ts +12 -3
- package/lib/reader-base-path.ts +24 -0
- package/lib/reader-static-params.ts +13 -0
- package/lib/reference-policy.ts +5 -0
- package/lib/relevance-decisions.ts +103 -0
- package/lib/relevance-status.ts +63 -0
- package/lib/renderer.ts +7 -0
- package/lib/revalidate-secret.ts +59 -0
- package/lib/robots-policy.ts +14 -0
- package/lib/root-route-redirect.ts +16 -0
- package/lib/semantic-graph.ts +7 -0
- package/lib/site-brand.ts +51 -0
- package/lib/staging-gated-path.ts +23 -0
- package/lib/staging-preview-object.ts +114 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +77 -0
- package/middleware.ts +23 -18
- package/package.json +45 -1
- package/renderers/pretext/article-page.tsx +39 -3
- package/renderers/pretext/presentation-shared.tsx +113 -0
- package/renderers/pretext/presentation-shell.tsx +15 -102
- package/renderers/pretext/rhythm-article-page.tsx +165 -0
- package/renderers/pretext/rhythm-blog-presentation.tsx +279 -0
- package/renderers/pretext/rhythm-shell-style.ts +35 -0
- package/routes.manifest.json +45 -2
- package/scripts/videoml/build-browser-bundle.mjs +22 -0
- package/scripts/videoml/build-preview-bundle.mjs +50 -0
- package/scripts/videoml/bundle-common.mjs +129 -0
- package/scripts/videoml/preview/preview-entry.tsx +381 -0
- package/scripts/videoml/preview/preview-stage-fit.ts +28 -0
- package/scripts/videoml/preview/preview-timing.ts +42 -0
- package/scripts/videoml/preview.html +77 -0
- package/scripts/videoml/standard-entry.tsx +46 -0
- package/src/sync.mjs +30 -6
- package/src/with-papyrus.mjs +19 -2
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +0 -193
- package/amplify/functions/graphql-jwt-authorizer/resource.js +0 -14
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +0 -15
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { toPublicNewsroomPath } from "./newsroom-base-path";
|
|
2
|
+
|
|
1
3
|
export const FORUM_THREAD_ROUTE_PREFIX = "/newsroom/forum/";
|
|
2
4
|
export const INSIGHT_THREAD_ROUTE_PREFIX = "/newsroom/insights/";
|
|
3
5
|
export const FORUM_MESSAGE_HASH_PREFIX = "message-";
|
|
@@ -81,7 +83,7 @@ export function buildForumThreadUrl(
|
|
|
81
83
|
if (options?.demo) params.set("demo", "1");
|
|
82
84
|
const query = params.toString();
|
|
83
85
|
const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
|
|
84
|
-
return `/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}
|
|
86
|
+
return toPublicNewsroomPath(`/newsroom/forum/${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
export function buildInsightThreadUrl(
|
|
@@ -93,7 +95,7 @@ export function buildInsightThreadUrl(
|
|
|
93
95
|
if (options?.demo) params.set("demo", "1");
|
|
94
96
|
const query = params.toString();
|
|
95
97
|
const hash = options?.messageId ? getForumMessageAnchorId(options.messageId) : "";
|
|
96
|
-
return `${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}
|
|
98
|
+
return toPublicNewsroomPath(`${INSIGHT_THREAD_ROUTE_PREFIX}${encodedThreadId}${query ? `?${query}` : ""}${hash ? `#${hash}` : ""}`);
|
|
97
99
|
}
|
|
98
100
|
|
|
99
101
|
export function buildThreadUrlForSurface(
|
|
@@ -141,7 +143,7 @@ export function buildNewsroomForumIndexUrl(demo?: boolean): string {
|
|
|
141
143
|
const params = new URLSearchParams();
|
|
142
144
|
if (demo) params.set("demo", "1");
|
|
143
145
|
const query = params.toString();
|
|
144
|
-
return query ? `/newsroom?${query}` : "/newsroom";
|
|
146
|
+
return toPublicNewsroomPath(query ? `/newsroom?${query}` : "/newsroom");
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string): string {
|
|
@@ -149,5 +151,5 @@ export function buildNewsroomInsightsIndexUrl(demo?: boolean, domain?: string):
|
|
|
149
151
|
if (demo) params.set("demo", "1");
|
|
150
152
|
if (domain?.trim()) params.set("domain", domain.trim());
|
|
151
153
|
const query = params.toString();
|
|
152
|
-
return query ? `/newsroom/insights?${query}` : "/newsroom/insights";
|
|
154
|
+
return toPublicNewsroomPath(query ? `/newsroom/insights?${query}` : "/newsroom/insights");
|
|
153
155
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { browserPathToInternalNewsroomPath, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
1
2
|
export type NewsroomIndexTab = "references" | "messages" | "assignments" | "insights";
|
|
2
3
|
|
|
3
4
|
export type ReferenceIndexOrder = "published" | "imported";
|
|
@@ -147,10 +148,10 @@ export function buildNewsroomIndexWebPath(
|
|
|
147
148
|
tab: NewsroomIndexTab,
|
|
148
149
|
filters: ReferencesIndexFilters | MessagesIndexFilters | AssignmentsIndexFilters | InsightsIndexFilters,
|
|
149
150
|
): string {
|
|
150
|
-
if (tab === "references") return `/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}
|
|
151
|
-
if (tab === "messages") return `/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}
|
|
152
|
-
if (tab === "insights") return `/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}
|
|
153
|
-
return `/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}
|
|
151
|
+
if (tab === "references") return toPublicNewsroomPath(`/newsroom/references${buildReferencesIndexQuery(filters as ReferencesIndexFilters)}`);
|
|
152
|
+
if (tab === "messages") return toPublicNewsroomPath(`/newsroom/messages${buildMessagesIndexQuery(filters as MessagesIndexFilters)}`);
|
|
153
|
+
if (tab === "insights") return toPublicNewsroomPath(`/newsroom/insights${buildInsightsIndexQuery(filters as InsightsIndexFilters)}`);
|
|
154
|
+
return toPublicNewsroomPath(`/newsroom/assignments${buildAssignmentsIndexQuery(filters as AssignmentsIndexFilters)}`);
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
export function syncBrowserNewsroomIndexUrl(
|
|
@@ -168,8 +169,9 @@ export function syncBrowserNewsroomIndexUrl(
|
|
|
168
169
|
|
|
169
170
|
/** Lineage id from `/newsroom/references/<id>` (not index-only URLs). */
|
|
170
171
|
export function parseReferenceLineageIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
173
|
+
if (!internalPathname?.startsWith("/newsroom/references/")) return null;
|
|
174
|
+
const segment = internalPathname.slice("/newsroom/references/".length).split("/")[0]?.trim() ?? "";
|
|
173
175
|
if (!segment) return null;
|
|
174
176
|
try {
|
|
175
177
|
return decodeURIComponent(segment);
|
|
@@ -180,8 +182,9 @@ export function parseReferenceLineageIdFromNewsroomPathname(pathname: string | n
|
|
|
180
182
|
|
|
181
183
|
/** Assignment id from `/newsroom/assignments/<id>` (not index-only URLs). */
|
|
182
184
|
export function parseAssignmentIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
186
|
+
if (!internalPathname?.startsWith("/newsroom/assignments/")) return null;
|
|
187
|
+
const segment = internalPathname.slice("/newsroom/assignments/".length).split("/")[0]?.trim() ?? "";
|
|
185
188
|
if (!segment) return null;
|
|
186
189
|
try {
|
|
187
190
|
return decodeURIComponent(segment);
|
|
@@ -191,8 +194,9 @@ export function parseAssignmentIdFromNewsroomPathname(pathname: string | null |
|
|
|
191
194
|
}
|
|
192
195
|
|
|
193
196
|
export function parseInsightThreadIdFromNewsroomPathname(pathname: string | null | undefined): string | null {
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
const internalPathname = pathname ? browserPathToInternalNewsroomPath(pathname) : null;
|
|
198
|
+
if (!internalPathname?.startsWith("/newsroom/insights/")) return null;
|
|
199
|
+
const segment = internalPathname.slice("/newsroom/insights/".length).split("/")[0]?.trim() ?? "";
|
|
196
200
|
if (!segment) return null;
|
|
197
201
|
try {
|
|
198
202
|
return decodeURIComponent(segment);
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { generateClient } from "aws-amplify/data";
|
|
2
|
+
import { getUrl, uploadData } from "aws-amplify/storage";
|
|
3
|
+
import type { Schema } from "../amplify/data/resource";
|
|
4
|
+
import type { MediaAssetFields, MediaAssetRecord, NewsroomMediaBackend } from "./newsroom-media";
|
|
5
|
+
|
|
6
|
+
type MediaListPage = {
|
|
7
|
+
data?: (Record<string, unknown> | null)[] | null;
|
|
8
|
+
nextToken?: string | null;
|
|
9
|
+
errors?: { message?: string }[] | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type MediaDataClient = {
|
|
13
|
+
models: {
|
|
14
|
+
MediaAsset: {
|
|
15
|
+
create: (
|
|
16
|
+
input: MediaAssetFields,
|
|
17
|
+
options: { authMode: "userPool" },
|
|
18
|
+
) => Promise<{ errors?: { message?: string }[] | null }>;
|
|
19
|
+
listMediaAssetsByItemAndSortKey: (
|
|
20
|
+
input: { itemId: string },
|
|
21
|
+
options: { authMode: "userPool"; nextToken?: string | null; limit?: number },
|
|
22
|
+
) => Promise<MediaListPage>;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type MediaStorageApi = {
|
|
28
|
+
upload: (input: { path: string; data: Blob; contentType: string }) => Promise<void>;
|
|
29
|
+
signedUrl: (path: string) => Promise<string>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function failOnErrors(errors: { message?: string }[] | null | undefined): void {
|
|
33
|
+
if (errors && errors.length > 0) {
|
|
34
|
+
throw new Error(errors.map((error) => error.message ?? "GraphQL error").join("; "));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function srcPathFromMetadata(metadata: unknown): string | null {
|
|
39
|
+
let parsed = metadata;
|
|
40
|
+
if (typeof metadata === "string") {
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(metadata);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
48
|
+
const srcPath = (parsed as Record<string, unknown>).srcPath;
|
|
49
|
+
return typeof srcPath === "string" ? srcPath : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createMediaBackend(client: () => MediaDataClient, storage: MediaStorageApi): NewsroomMediaBackend {
|
|
53
|
+
return {
|
|
54
|
+
uploadImage: ({ storagePath, data, contentType }) =>
|
|
55
|
+
storage.upload({ path: storagePath, data, contentType }),
|
|
56
|
+
async createMediaAsset(fields) {
|
|
57
|
+
const response = await client().models.MediaAsset.create(fields, { authMode: "userPool" });
|
|
58
|
+
failOnErrors(response.errors);
|
|
59
|
+
},
|
|
60
|
+
async listMediaAssets(itemId) {
|
|
61
|
+
const records: MediaAssetRecord[] = [];
|
|
62
|
+
let nextToken: string | null | undefined = null;
|
|
63
|
+
do {
|
|
64
|
+
const page: MediaListPage = await client().models.MediaAsset.listMediaAssetsByItemAndSortKey(
|
|
65
|
+
{ itemId },
|
|
66
|
+
{ authMode: "userPool", nextToken, limit: 500 },
|
|
67
|
+
);
|
|
68
|
+
failOnErrors(page.errors);
|
|
69
|
+
for (const row of page.data ?? []) {
|
|
70
|
+
if (!row) continue;
|
|
71
|
+
records.push({
|
|
72
|
+
id: String(row.id),
|
|
73
|
+
sortKey: String(row.sortKey),
|
|
74
|
+
storagePath: typeof row.storagePath === "string" ? row.storagePath : null,
|
|
75
|
+
alt: typeof row.alt === "string" ? row.alt : "",
|
|
76
|
+
srcPath: srcPathFromMetadata(row.metadata),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
nextToken = page.nextToken;
|
|
80
|
+
} while (nextToken);
|
|
81
|
+
return records;
|
|
82
|
+
},
|
|
83
|
+
imageUrl: (storagePath) => storage.signedUrl(storagePath),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const amplifyStorage: MediaStorageApi = {
|
|
88
|
+
async upload({ path, data, contentType }) {
|
|
89
|
+
await uploadData({ path, data, options: { contentType } }).result;
|
|
90
|
+
},
|
|
91
|
+
async signedUrl(path) {
|
|
92
|
+
const { url } = await getUrl({ path });
|
|
93
|
+
return url.toString();
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export function createLiveMediaBackend(): NewsroomMediaBackend {
|
|
98
|
+
let client: MediaDataClient | null = null;
|
|
99
|
+
return createMediaBackend(() => {
|
|
100
|
+
if (!client) client = generateClient<Schema>() as unknown as MediaDataClient;
|
|
101
|
+
return client;
|
|
102
|
+
}, amplifyStorage);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function createDemoMediaBackend(): NewsroomMediaBackend {
|
|
106
|
+
const objects = new Map<string, Blob>();
|
|
107
|
+
const rows = new Map<string, MediaAssetFields[]>();
|
|
108
|
+
return {
|
|
109
|
+
async uploadImage({ storagePath, data }) {
|
|
110
|
+
objects.set(storagePath, data);
|
|
111
|
+
},
|
|
112
|
+
async createMediaAsset(fields) {
|
|
113
|
+
rows.set(fields.itemId, [...(rows.get(fields.itemId) ?? []), fields]);
|
|
114
|
+
},
|
|
115
|
+
async listMediaAssets(itemId) {
|
|
116
|
+
return (rows.get(itemId) ?? []).map((fields) => ({
|
|
117
|
+
id: fields.id,
|
|
118
|
+
sortKey: fields.sortKey,
|
|
119
|
+
storagePath: fields.storagePath,
|
|
120
|
+
alt: fields.alt,
|
|
121
|
+
srcPath: srcPathFromMetadata(fields.metadata),
|
|
122
|
+
}));
|
|
123
|
+
},
|
|
124
|
+
async imageUrl(storagePath) {
|
|
125
|
+
const object = objects.get(storagePath);
|
|
126
|
+
if (!object) throw new Error(`No demo object at ${storagePath}.`);
|
|
127
|
+
return URL.createObjectURL(object);
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
let liveBackend: NewsroomMediaBackend | null = null;
|
|
133
|
+
let demoBackend: NewsroomMediaBackend | null = null;
|
|
134
|
+
|
|
135
|
+
export function resolveMediaBackend(demo: boolean): NewsroomMediaBackend {
|
|
136
|
+
if (demo) {
|
|
137
|
+
demoBackend ??= createDemoMediaBackend();
|
|
138
|
+
return demoBackend;
|
|
139
|
+
}
|
|
140
|
+
liveBackend ??= createLiveMediaBackend();
|
|
141
|
+
return liveBackend;
|
|
142
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export const MAX_IMAGE_BYTES = 10 * 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
export const IMAGE_CONTENT_TYPES: Record<string, string> = {
|
|
4
|
+
png: "image/png",
|
|
5
|
+
jpg: "image/jpeg",
|
|
6
|
+
jpeg: "image/jpeg",
|
|
7
|
+
gif: "image/gif",
|
|
8
|
+
webp: "image/webp",
|
|
9
|
+
avif: "image/avif",
|
|
10
|
+
svg: "image/svg+xml",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type ImageFileLike = { name: string; size: number };
|
|
14
|
+
|
|
15
|
+
function extensionOf(filename: string): string {
|
|
16
|
+
const dot = filename.lastIndexOf(".");
|
|
17
|
+
return dot === -1 ? "" : filename.slice(dot + 1).toLowerCase();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function contentTypeForFilename(filename: string): string | null {
|
|
21
|
+
return IMAGE_CONTENT_TYPES[extensionOf(filename)] ?? null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isSvgFilename(filename: string): boolean {
|
|
25
|
+
return extensionOf(filename) === "svg";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function sanitizeSegment(value: string): string {
|
|
29
|
+
return value
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/[^a-z0-9._-]+/g, "-")
|
|
32
|
+
.replace(/-{2,}/g, "-")
|
|
33
|
+
.replace(/^[-.]+|-+$/g, "");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function srcPathFor(slug: string, filename: string, existingSrcPaths: Iterable<string> = []): string {
|
|
37
|
+
const taken = new Set(existingSrcPaths);
|
|
38
|
+
const extension = sanitizeSegment(extensionOf(filename));
|
|
39
|
+
const dot = filename.lastIndexOf(".");
|
|
40
|
+
const rawStem = dot === -1 ? filename : filename.slice(0, dot);
|
|
41
|
+
const stem = sanitizeSegment(rawStem) || "image";
|
|
42
|
+
const directory = sanitizeSegment(slug) || "article";
|
|
43
|
+
const suffix = extension ? `.${extension}` : "";
|
|
44
|
+
let candidate = `assets/${directory}/${stem}${suffix}`;
|
|
45
|
+
for (let counter = 2; taken.has(candidate); counter += 1) {
|
|
46
|
+
candidate = `assets/${directory}/${stem}-${counter}${suffix}`;
|
|
47
|
+
}
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function sha256Hex(data: string | ArrayBuffer): Promise<string> {
|
|
52
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
53
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
54
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function mediaIdFor(itemId: string, srcPath: string): Promise<string> {
|
|
58
|
+
return `media-${itemId}-${(await sha256Hex(srcPath)).slice(0, 10)}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function validateImageFile(file: ImageFileLike): string | null {
|
|
62
|
+
if (contentTypeForFilename(file.name) === null) {
|
|
63
|
+
return "Choose a PNG, JPEG, GIF, WebP, AVIF or SVG image.";
|
|
64
|
+
}
|
|
65
|
+
if (file.size <= 0) return "That file is empty.";
|
|
66
|
+
if (file.size > MAX_IMAGE_BYTES) return "Images can be at most 10 MB.";
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function altAttributeValue(alt: string): string {
|
|
71
|
+
return alt.replace(/[\r\n]+/g, " ").replace(/"/g, "'").replace(/[{}]/g, "");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function imageDirective(srcPath: string, alt: string): string {
|
|
75
|
+
return `::image{src="${srcPath}" alt="${altAttributeValue(alt)}" layout="inline"}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type MediaAssetFields = {
|
|
79
|
+
id: string;
|
|
80
|
+
itemId: string;
|
|
81
|
+
type: "image";
|
|
82
|
+
role: "body";
|
|
83
|
+
sortKey: string;
|
|
84
|
+
storagePath: string;
|
|
85
|
+
alt: string;
|
|
86
|
+
width?: number;
|
|
87
|
+
height?: number;
|
|
88
|
+
metadata: string;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export function mediaSortKey(position: number, srcPath: string): string {
|
|
92
|
+
return `${String(position).padStart(3, "0")}#${srcPath}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type SelectionInsertion = { body: string; altSelectionStart: number };
|
|
96
|
+
|
|
97
|
+
function countEdgeNewlines(text: string, fromEnd: boolean): number {
|
|
98
|
+
let count = 0;
|
|
99
|
+
while (count < 2 && (fromEnd ? text[text.length - 1 - count] : text[count]) === "\n") count += 1;
|
|
100
|
+
return count;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function insertDirectiveAtSelection(
|
|
104
|
+
body: string,
|
|
105
|
+
selectionStart: number,
|
|
106
|
+
selectionEnd: number,
|
|
107
|
+
directive: string,
|
|
108
|
+
): SelectionInsertion {
|
|
109
|
+
const before = body.slice(0, selectionStart);
|
|
110
|
+
const after = body.slice(selectionEnd);
|
|
111
|
+
const prefix = before === "" ? "" : "\n".repeat(2 - countEdgeNewlines(before, true));
|
|
112
|
+
const suffix = after === "" ? "\n" : "\n".repeat(2 - countEdgeNewlines(after, false));
|
|
113
|
+
const directiveStart = before.length + prefix.length;
|
|
114
|
+
return {
|
|
115
|
+
body: `${before}${prefix}${directive}${suffix}${after}`,
|
|
116
|
+
altSelectionStart: directiveStart + directive.indexOf('alt="') + 'alt="'.length,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type MediaAssetRecord = {
|
|
121
|
+
id: string;
|
|
122
|
+
sortKey: string;
|
|
123
|
+
storagePath: string | null;
|
|
124
|
+
alt: string;
|
|
125
|
+
srcPath: string | null;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export type NewsroomMediaBackend = {
|
|
129
|
+
uploadImage: (input: { storagePath: string; data: Blob; contentType: string }) => Promise<void>;
|
|
130
|
+
createMediaAsset: (fields: MediaAssetFields) => Promise<void>;
|
|
131
|
+
listMediaAssets: (itemId: string) => Promise<MediaAssetRecord[]>;
|
|
132
|
+
imageUrl: (storagePath: string) => Promise<string>;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export type ImageDimensionReader = (file: Blob, filename: string) => Promise<{ width: number; height: number } | null>;
|
|
136
|
+
|
|
137
|
+
export type UploadedImage = { srcPath: string; directive: string; imageUrl: string };
|
|
138
|
+
|
|
139
|
+
export async function uploadArticleImage(
|
|
140
|
+
backend: NewsroomMediaBackend,
|
|
141
|
+
readDimensions: ImageDimensionReader,
|
|
142
|
+
input: { itemId: string; slug: string; file: File },
|
|
143
|
+
): Promise<UploadedImage> {
|
|
144
|
+
const problem = validateImageFile(input.file);
|
|
145
|
+
if (problem) throw new Error(problem);
|
|
146
|
+
const contentType = contentTypeForFilename(input.file.name) as string;
|
|
147
|
+
const existing = await backend.listMediaAssets(input.itemId);
|
|
148
|
+
const srcPath = srcPathFor(
|
|
149
|
+
input.slug,
|
|
150
|
+
input.file.name,
|
|
151
|
+
existing.map((asset) => asset.srcPath).filter((path): path is string => path !== null),
|
|
152
|
+
);
|
|
153
|
+
const bytes = await input.file.arrayBuffer();
|
|
154
|
+
const sha256 = await sha256Hex(bytes);
|
|
155
|
+
const dimensions = await readDimensions(input.file, input.file.name);
|
|
156
|
+
const storagePath = `media/${srcPath}`;
|
|
157
|
+
await backend.uploadImage({ storagePath, data: input.file, contentType });
|
|
158
|
+
await backend.createMediaAsset({
|
|
159
|
+
id: await mediaIdFor(input.itemId, srcPath),
|
|
160
|
+
itemId: input.itemId,
|
|
161
|
+
type: "image",
|
|
162
|
+
role: "body",
|
|
163
|
+
sortKey: mediaSortKey(existing.length + 1, srcPath),
|
|
164
|
+
storagePath,
|
|
165
|
+
alt: input.file.name,
|
|
166
|
+
...(dimensions ?? {}),
|
|
167
|
+
metadata: JSON.stringify({ srcPath, sha256, bytes: bytes.byteLength, contentType }),
|
|
168
|
+
});
|
|
169
|
+
return { srcPath, directive: imageDirective(srcPath, ""), imageUrl: await backend.imageUrl(storagePath) };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function readImageDimensions(file: Blob, filename: string): Promise<{ width: number; height: number } | null> {
|
|
173
|
+
if (isSvgFilename(filename)) return null;
|
|
174
|
+
try {
|
|
175
|
+
const bitmap = await createImageBitmap(file);
|
|
176
|
+
const size = { width: bitmap.width, height: bitmap.height };
|
|
177
|
+
bitmap.close();
|
|
178
|
+
return size;
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
package/lib/newsroom-nav.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { newsroomHref, toPublicNewsroomPath } from "./newsroom-base-path";
|
|
|
2
2
|
|
|
3
3
|
export type NewsroomNavTabId =
|
|
4
4
|
| "overview"
|
|
5
|
+
| "articles"
|
|
5
6
|
| "messages"
|
|
6
7
|
| "insights"
|
|
7
8
|
| "assignments"
|
|
@@ -22,12 +23,14 @@ export type NewsroomNavItem = {
|
|
|
22
23
|
|
|
23
24
|
export const NEWSROOM_OPS_NAV: NewsroomNavItem[] = [
|
|
24
25
|
{ id: "overview", label: "Overview", detail: "Desk home", href: newsroomHref(), mobilePrimary: true },
|
|
26
|
+
{ id: "articles", label: "Articles", detail: "Drafts & published", href: newsroomHref("articles"), mobilePrimary: true },
|
|
25
27
|
{ id: "assignments", label: "Assignments", detail: "Work queue", href: newsroomHref("assignments"), mobilePrimary: true },
|
|
26
28
|
{ id: "references", label: "References", detail: "Knowledge base", href: newsroomHref("references"), mobilePrimary: true },
|
|
27
29
|
{ id: "messages", label: "Messages", detail: "Forum", href: newsroomHref("messages") },
|
|
28
30
|
{ id: "insights", label: "Insights", detail: "Research threads", href: newsroomHref("insights") },
|
|
29
31
|
{ id: "topics", label: "Topics", detail: "Taxonomy", href: newsroomHref("topics"), mobilePrimary: true },
|
|
30
32
|
{ id: "concepts", label: "Concepts", detail: "Ontology", href: newsroomHref("concepts") },
|
|
33
|
+
{ id: "search", label: "Search", detail: "Knowledge base", href: newsroomHref("search") },
|
|
31
34
|
{ id: "administration", label: "Administration", detail: "Users & policies", href: newsroomHref("administration") },
|
|
32
35
|
];
|
|
33
36
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
function originOf(value: string): string {
|
|
2
|
+
const trimmed = value.replace(/\/+$/, "");
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(trimmed);
|
|
5
|
+
const hostname = parsed.hostname === "127.0.0.1" || parsed.hostname === "::1" ? "localhost" : parsed.hostname;
|
|
6
|
+
return `${parsed.protocol}//${hostname}${parsed.port ? `:${parsed.port}` : ""}`;
|
|
7
|
+
} catch {
|
|
8
|
+
return trimmed;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function pathOf(value: string): string | null {
|
|
13
|
+
try {
|
|
14
|
+
return new URL(value).pathname.replace(/\/+$/, "");
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Puts the redirect URLs of the current origin first; among those, the
|
|
22
|
+
* newsroom return path leads when the site has one, so Amplify (which signs in
|
|
23
|
+
* with the first entry) returns an editor to the newsroom.
|
|
24
|
+
*/
|
|
25
|
+
export function prioritizeRedirectUrlsForOrigin(
|
|
26
|
+
urls: readonly string[],
|
|
27
|
+
currentOrigin: string,
|
|
28
|
+
publicNewsroomBasePath: string,
|
|
29
|
+
): string[] {
|
|
30
|
+
const origin = originOf(currentOrigin);
|
|
31
|
+
const newsroomPath = publicNewsroomBasePath.replace(/\/+$/, "");
|
|
32
|
+
const head = urls.filter((value) => originOf(value) === origin);
|
|
33
|
+
if (head.length === 0) return [...urls];
|
|
34
|
+
const tail = urls.filter((value) => originOf(value) !== origin);
|
|
35
|
+
const newsroomFirst =
|
|
36
|
+
newsroomPath === ""
|
|
37
|
+
? head
|
|
38
|
+
: [...head.filter((value) => pathOf(value) === newsroomPath), ...head.filter((value) => pathOf(value) !== newsroomPath)];
|
|
39
|
+
return [...newsroomFirst, ...tail];
|
|
40
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { toInternalReaderPath, toPublicReaderPath } from "./reader-base-path";
|
|
1
2
|
import { PAPYRUS_OBJECT_KINDS, type PapyrusObjectKind } from "./papyrus-object-kinds";
|
|
2
3
|
import {
|
|
3
4
|
buildNewsroomIndexWebPath,
|
|
@@ -52,7 +53,7 @@ export function buildWebUiContext(webPath: string): PapyrusWebUiContext {
|
|
|
52
53
|
export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
|
|
53
54
|
const normalized = normalizeWebPath(webPath);
|
|
54
55
|
const url = new URL(normalized, "https://papyrus.local");
|
|
55
|
-
const pathname = url.pathname || "/";
|
|
56
|
+
const pathname = toInternalReaderPath(url.pathname || "/");
|
|
56
57
|
|
|
57
58
|
if (pathname === "/" || pathname === "") {
|
|
58
59
|
return location("papyrus://site/home", normalized, { label: "Papyrus home" });
|
|
@@ -81,7 +82,7 @@ export function webPathToPapyrusLocation(webPath: string): PapyrusWebUiContext {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
if (!pathname.startsWith("/newsroom")) {
|
|
84
|
-
const tail = pathname.replace(/^\/+/, "");
|
|
85
|
+
const tail = (url.pathname || "/").replace(/^\/+/, "");
|
|
85
86
|
return location(`papyrus://site/path/${encodeURIComponent(tail)}`, normalized, { label: pathname });
|
|
86
87
|
}
|
|
87
88
|
|
|
@@ -215,9 +216,9 @@ export function papyrusUriToWebPath(uri: string): PapyrusUriToWebPathResult {
|
|
|
215
216
|
const { kind, id } = parsed;
|
|
216
217
|
|
|
217
218
|
if (kind === "site") {
|
|
218
|
-
if (id === "home") return { ok: true, papyrusLocationUri: raw, webPath: "/" };
|
|
219
|
-
if (id === "archive") return { ok: true, papyrusLocationUri: raw, webPath: "/archive" };
|
|
220
|
-
if (id === "settings") return { ok: true, papyrusLocationUri: raw, webPath: "/settings" };
|
|
219
|
+
if (id === "home") return { ok: true, papyrusLocationUri: raw, webPath: toPublicReaderPath("/") };
|
|
220
|
+
if (id === "archive") return { ok: true, papyrusLocationUri: raw, webPath: toPublicReaderPath("/archive") };
|
|
221
|
+
if (id === "settings") return { ok: true, papyrusLocationUri: raw, webPath: toPublicReaderPath("/settings") };
|
|
221
222
|
if (id.startsWith("path/")) {
|
|
222
223
|
return { ok: true, papyrusLocationUri: raw, webPath: `/${decodeURIComponent(id.slice("path/".length))}` };
|
|
223
224
|
}
|
|
@@ -449,7 +450,7 @@ function objectKindToWebPath(kind: PapyrusObjectKind, objectId: string): string
|
|
|
449
450
|
if (kind === "category") return `/newsroom/topics?category=${encoded}`;
|
|
450
451
|
if (kind === "semanticNode") return `/newsroom/concepts?node=${encoded}`;
|
|
451
452
|
if (kind === "newsroomSection") return `/newsroom/sections/${encoded}`;
|
|
452
|
-
if (kind === "item") return `/articles/${encoded}
|
|
453
|
+
if (kind === "item") return toPublicReaderPath(`/articles/${encoded}`);
|
|
453
454
|
return `/newsroom?object=${encodeURIComponent(kind)}:${encoded}`;
|
|
454
455
|
}
|
|
455
456
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getEditionSectionItems } from "./edition-sections";
|
|
2
2
|
import type { EditionContent } from "./content-types";
|
|
3
3
|
import type { PublicationItem } from "./publication-items";
|
|
4
|
+
import { toPublicReaderPath } from "./reader-base-path";
|
|
4
5
|
|
|
5
6
|
export type PresentationFooterEntry = {
|
|
6
7
|
section: string;
|
|
@@ -43,9 +44,9 @@ export type PresentationFooterGeometry = {
|
|
|
43
44
|
};
|
|
44
45
|
|
|
45
46
|
export const PRESENTATION_FOOTER_UTILITIES: PresentationFooterUtilityEntry[] = [
|
|
46
|
-
{ id: "archive", label: "Archive", href: "/archive", disabled: false },
|
|
47
|
+
{ id: "archive", label: "Archive", href: toPublicReaderPath("/archive"), disabled: false },
|
|
47
48
|
{ id: "newsDesk", label: "Newsroom", href: "/newsroom", disabled: false },
|
|
48
|
-
{ id: "settings", label: "Settings", href: "/settings", disabled: false },
|
|
49
|
+
{ id: "settings", label: "Settings", href: toPublicReaderPath("/settings"), disabled: false },
|
|
49
50
|
{ id: "login", label: "LOGIN", disabled: true },
|
|
50
51
|
];
|
|
51
52
|
|
package/lib/pretext-layout.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
clearCache,
|
|
2
3
|
layoutNextLine,
|
|
3
4
|
prepareWithSegments,
|
|
4
5
|
type LayoutCursor,
|
|
@@ -78,14 +79,18 @@ export function layoutAllTextLines({
|
|
|
78
79
|
linePaintHeight,
|
|
79
80
|
fontSize,
|
|
80
81
|
fontFamily,
|
|
81
|
-
|
|
82
|
+
obstacles = [],
|
|
83
|
+
}: Omit<LayoutTextLinesOptions, "cursor" | "maxHeight" | "obstacles"> & { obstacles?: TextObstacle[] }): TextLine[] {
|
|
82
84
|
const lines: TextLine[] = [];
|
|
83
85
|
let cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 };
|
|
84
86
|
|
|
85
87
|
for (let lineIndex = 0; lineIndex < 2000; lineIndex += 1) {
|
|
86
|
-
const
|
|
88
|
+
const y = lineIndex * lineHeight;
|
|
89
|
+
const slot = getAvailableSlot(maxWidth, y, linePaintHeight, obstacles);
|
|
90
|
+
if (!slot) continue;
|
|
91
|
+
const line = layoutNextLine(prepared, cursor, slot.width);
|
|
87
92
|
if (!line) return lines;
|
|
88
|
-
lines.push(toTextLine(line,
|
|
93
|
+
lines.push(toTextLine(line, slot.x, y, fontSize, fontFamily, lineHeight, linePaintHeight));
|
|
89
94
|
cursor = line.end;
|
|
90
95
|
}
|
|
91
96
|
|
|
@@ -177,3 +182,7 @@ function toTextLine(line: LayoutLine, x: number, y: number, fontSize: number, fo
|
|
|
177
182
|
function clamp(value: number, min: number, max: number): number {
|
|
178
183
|
return Math.max(min, Math.min(max, value));
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
export function clearPretextMeasurementCache(): void {
|
|
187
|
+
clearCache();
|
|
188
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { SITE_BRAND } from "./site-brand";
|
|
2
|
+
|
|
3
|
+
export function normalizeReaderBasePath(value: string | undefined | null): string {
|
|
4
|
+
const trimmed = (value ?? "").trim().replace(/\/+$/, "");
|
|
5
|
+
if (!trimmed) return "";
|
|
6
|
+
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getReaderBasePath(): string {
|
|
10
|
+
return normalizeReaderBasePath(SITE_BRAND.readerBasePath);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toPublicReaderPath(path: string, readerBasePath: string = getReaderBasePath()): string {
|
|
14
|
+
if (!readerBasePath) return path;
|
|
15
|
+
if (path === "/" || path === "") return readerBasePath;
|
|
16
|
+
return `${readerBasePath}${path.startsWith("/") ? path : `/${path}`}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function toInternalReaderPath(path: string, readerBasePath: string = getReaderBasePath()): string {
|
|
20
|
+
if (!readerBasePath) return path;
|
|
21
|
+
if (path === readerBasePath || path === `${readerBasePath}/`) return "/";
|
|
22
|
+
if (path.startsWith(`${readerBasePath}/`)) return path.slice(readerBasePath.length);
|
|
23
|
+
return path;
|
|
24
|
+
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { graphqlContentRepository } from "./graphql-content-repository";
|
|
2
|
+
import { SITE_BRAND } from "./site-brand";
|
|
3
|
+
import { getContentSource } from "./site-env";
|
|
4
|
+
import type { RendererConfig } from "./renderer-config";
|
|
5
|
+
|
|
6
|
+
export function siteEnumeratesReaderRoutesAtBuild(): boolean {
|
|
7
|
+
return siteServesNextReaderRoutes() && getContentSource() !== "drafts";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function siteServesNextReaderRoutes(rendererKind: RendererConfig["kind"] = SITE_BRAND.renderer.kind): boolean {
|
|
11
|
+
return rendererKind !== "markus";
|
|
12
|
+
}
|
|
2
13
|
|
|
3
14
|
const MONTH_NAMES = [
|
|
4
15
|
"january",
|
|
@@ -23,6 +34,7 @@ type EditionDateStaticParam = {
|
|
|
23
34
|
|
|
24
35
|
export async function generateEditionDateStaticParams(): Promise<EditionDateStaticParam[]> {
|
|
25
36
|
if (process.env.NODE_ENV === "development") return [];
|
|
37
|
+
if (!siteEnumeratesReaderRoutesAtBuild()) return [];
|
|
26
38
|
|
|
27
39
|
const { editions } = await graphqlContentRepository.listPublishedEditions({ limit: 100 });
|
|
28
40
|
const params: EditionDateStaticParam[] = [];
|
|
@@ -35,6 +47,7 @@ export async function generateEditionDateStaticParams(): Promise<EditionDateStat
|
|
|
35
47
|
|
|
36
48
|
export async function generateArticleStaticParams(): Promise<Array<{ slug: string }>> {
|
|
37
49
|
if (process.env.NODE_ENV === "development") return [];
|
|
50
|
+
if (!siteEnumeratesReaderRoutesAtBuild()) return [];
|
|
38
51
|
|
|
39
52
|
const slugs = await graphqlContentRepository.listArticleSlugs();
|
|
40
53
|
return slugs.map((slug) => ({ slug }));
|
package/lib/reference-policy.ts
CHANGED
|
@@ -17,6 +17,11 @@ export type ReferenceRejectionReasonCode = typeof REFERENCE_REJECTION_REASON_COD
|
|
|
17
17
|
const REFERENCE_REJECTION_REASON_CODE_SET = new Set<string>(REFERENCE_REJECTION_REASON_CODES);
|
|
18
18
|
const SCOPE_TRAINING_NEGATIVE_REASON_CODE_SET = new Set<string>(["out_of_scope", "policy_exclusion"]);
|
|
19
19
|
|
|
20
|
+
/** Rejection reasons that say the source is not relevant (scope-training negatives). */
|
|
21
|
+
export function isScopeTrainingNegativeReason(code: string): boolean {
|
|
22
|
+
return SCOPE_TRAINING_NEGATIVE_REASON_CODE_SET.has(code);
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
export function normalizeReferenceCurationStatus(value: unknown, defaultStatus: ReferenceCurationStatus = "pending"): ReferenceCurationStatus {
|
|
21
26
|
const normalized = normalizePolicyToken(value ?? defaultStatus);
|
|
22
27
|
if (normalized === "accepted" || normalized === "accept" || normalized === "ready" || normalized === "trusted") return "accepted";
|