@anthusai/papyrus 0.0.0 → 1.0.0-next.1

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 (254) hide show
  1. package/LICENSE +21 -0
  2. package/amplify/auth/resource.js +61 -0
  3. package/amplify/auth/resource.ts +65 -0
  4. package/amplify/data/resource.js +2037 -0
  5. package/amplify/data/resource.ts +2148 -0
  6. package/amplify/data/schema.js +1850 -0
  7. package/amplify/data/schema.ts +1953 -0
  8. package/amplify/functions/assignment-action/handler.ts +1416 -0
  9. package/amplify/functions/assignment-action/resource.js +8 -0
  10. package/amplify/functions/assignment-action/resource.ts +9 -0
  11. package/amplify/functions/category-action/handler.ts +2259 -0
  12. package/amplify/functions/category-action/resource.js +8 -0
  13. package/amplify/functions/category-action/resource.ts +9 -0
  14. package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
  15. package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
  16. package/amplify/functions/console-chat-responder/Dockerfile +19 -0
  17. package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
  18. package/amplify/functions/console-chat-responder/resource.js +153 -0
  19. package/amplify/functions/console-chat-responder/resource.ts +179 -0
  20. package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
  21. package/amplify/functions/email-submission-processor/handler.py +46 -0
  22. package/amplify/functions/email-submission-processor/requirements.txt +2 -0
  23. package/amplify/functions/email-submission-processor/resource.js +56 -0
  24. package/amplify/functions/email-submission-processor/resource.ts +69 -0
  25. package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
  26. package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
  27. package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
  28. package/amplify/functions/knowledge-query/handler.py +1 -0
  29. package/amplify/functions/knowledge-query/requirements.txt +3 -0
  30. package/amplify/functions/knowledge-query/resource.js +50 -0
  31. package/amplify/functions/knowledge-query/resource.ts +61 -0
  32. package/amplify/functions/manage-user-role/handler.ts +799 -0
  33. package/amplify/functions/manage-user-role/resource.js +8 -0
  34. package/amplify/functions/manage-user-role/resource.ts +9 -0
  35. package/amplify/functions/model-attachment-upload/handler.ts +387 -0
  36. package/amplify/functions/model-attachment-upload/resource.js +8 -0
  37. package/amplify/functions/model-attachment-upload/resource.ts +9 -0
  38. package/amplify/functions/newsroom-summary/handler.ts +321 -0
  39. package/amplify/functions/newsroom-summary/resource.js +8 -0
  40. package/amplify/functions/newsroom-summary/resource.ts +9 -0
  41. package/amplify/functions/procedure-action/handler.ts +621 -0
  42. package/amplify/functions/procedure-action/resource.js +8 -0
  43. package/amplify/functions/procedure-action/resource.ts +9 -0
  44. package/amplify/functions/reader-settings/handler.ts +322 -0
  45. package/amplify/functions/reader-settings/resource.js +8 -0
  46. package/amplify/functions/reader-settings/resource.ts +9 -0
  47. package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
  48. package/amplify/functions/ses-inbound-receive/resource.js +8 -0
  49. package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
  50. package/amplify/functions/shared/amplify-secrets.ts +65 -0
  51. package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
  52. package/amplify/functions/shared/email-mime-intake.ts +196 -0
  53. package/amplify/functions/shared/email-submission.ts +415 -0
  54. package/amplify/functions/shared/inbound-email-intake.ts +37 -0
  55. package/amplify/functions/shared/lambda-data-client.ts +21 -0
  56. package/amplify/functions/shared/model-payloads.ts +186 -0
  57. package/amplify/functions/shared/python-bundle.js +115 -0
  58. package/amplify/functions/shared/python-bundle.ts +141 -0
  59. package/amplify/functions/shared/slack-events.ts +111 -0
  60. package/amplify/functions/slack-delivery/handler.py +112 -0
  61. package/amplify/functions/slack-delivery/requirements.txt +2 -0
  62. package/amplify/functions/slack-delivery/resource.js +38 -0
  63. package/amplify/functions/slack-delivery/resource.ts +51 -0
  64. package/amplify/functions/slack-events/handler.ts +155 -0
  65. package/amplify/functions/slack-events/resource.js +11 -0
  66. package/amplify/functions/slack-events/resource.ts +12 -0
  67. package/amplify/inbound-email/stack.js +46 -0
  68. package/amplify/inbound-email/stack.ts +61 -0
  69. package/amplify/package.json +3 -0
  70. package/amplify/site-backend-config.js +1 -0
  71. package/amplify/site-backend-config.ts +30 -0
  72. package/amplify/site-backend.js +587 -0
  73. package/amplify/site-backend.ts +704 -0
  74. package/amplify/storage/resource.js +18 -0
  75. package/amplify/storage/resource.ts +19 -0
  76. package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
  77. package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
  78. package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
  79. package/app/[year]/[month]/[day]/page.tsx +22 -0
  80. package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
  81. package/app/api/archive/editions/route.ts +18 -0
  82. package/app/api/media/[...path]/route.ts +35 -0
  83. package/app/api/revalidate/route.ts +94 -0
  84. package/app/archive/page.tsx +24 -0
  85. package/app/articles/[slug]/page.tsx +35 -0
  86. package/app/globals.css +10329 -0
  87. package/app/layout.tsx +146 -0
  88. package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
  89. package/app/newsroom/[section]/page.tsx +47 -0
  90. package/app/newsroom/forum/[threadId]/page.tsx +35 -0
  91. package/app/newsroom/insights/[threadId]/page.tsx +22 -0
  92. package/app/newsroom/insights/page.tsx +14 -0
  93. package/app/newsroom/layout.tsx +12 -0
  94. package/app/newsroom/page.tsx +31 -0
  95. package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
  96. package/app/page.tsx +272 -0
  97. package/app/settings/page.tsx +14 -0
  98. package/app/tailwind.css +65 -0
  99. package/bin/papyrus-app.mjs +10 -0
  100. package/bin/papyrus-infra.mjs +39 -0
  101. package/components/ai-elements/conversation.tsx +168 -0
  102. package/components/ai-elements/message.tsx +360 -0
  103. package/components/ai-elements/model-selector.tsx +30 -0
  104. package/components/ai-elements/prompt-input.tsx +1461 -0
  105. package/components/ai-elements/shimmer.tsx +76 -0
  106. package/components/ai-elements/suggestion.tsx +54 -0
  107. package/components/ai-elements/tool.tsx +173 -0
  108. package/components/amplify-client-provider.tsx +140 -0
  109. package/components/archive-grid.tsx +89 -0
  110. package/components/archive-shell.tsx +41 -0
  111. package/components/article-video.tsx +47 -0
  112. package/components/blog-page-background.tsx +20 -0
  113. package/components/dev-sandbox-editor-sign-in.tsx +60 -0
  114. package/components/news-desk-client-provider.tsx +211 -0
  115. package/components/news-desk-page.tsx +119 -0
  116. package/components/news-desk-taxonomy-client.ts +3684 -0
  117. package/components/newspaper.tsx +1929 -0
  118. package/components/newsroom-assignments-view.tsx +664 -0
  119. package/components/newsroom-ops-overview.tsx +226 -0
  120. package/components/newsroom-ops-shell.tsx +319 -0
  121. package/components/newsroom-references-view.tsx +598 -0
  122. package/components/newsroom-topics-view.tsx +425 -0
  123. package/components/papyrus-console-shell.tsx +1912 -0
  124. package/components/pictogram-figure.tsx +65 -0
  125. package/components/presentation-footer.tsx +146 -0
  126. package/components/reader-auth-control.tsx +162 -0
  127. package/components/reader-auth-state.ts +122 -0
  128. package/components/reader-settings.ts +332 -0
  129. package/components/reader-theme-control.tsx +85 -0
  130. package/components/reference-source-preview.tsx +163 -0
  131. package/components/settings-page.tsx +239 -0
  132. package/components/topic-steering-workspace.tsx +18063 -0
  133. package/components/ui/badge.tsx +30 -0
  134. package/components/ui/button-group.tsx +87 -0
  135. package/components/ui/button.tsx +58 -0
  136. package/components/ui/card.tsx +32 -0
  137. package/components/ui/command.tsx +194 -0
  138. package/components/ui/dialog.tsx +158 -0
  139. package/components/ui/dropdown-menu.tsx +266 -0
  140. package/components/ui/hover-card.tsx +51 -0
  141. package/components/ui/input-group.tsx +158 -0
  142. package/components/ui/input.tsx +20 -0
  143. package/components/ui/scroll-area.tsx +16 -0
  144. package/components/ui/select.tsx +199 -0
  145. package/components/ui/separator.tsx +23 -0
  146. package/components/ui/sheet.tsx +92 -0
  147. package/components/ui/spinner.tsx +10 -0
  148. package/components/ui/tabs.tsx +113 -0
  149. package/components/ui/textarea.tsx +18 -0
  150. package/components/ui/tooltip.tsx +66 -0
  151. package/components/use-resolved-papyrus-theme.ts +47 -0
  152. package/infra/amplify-app-shell.js +109 -0
  153. package/infra/amplify-app-shell.ts +155 -0
  154. package/infra/index.js +12 -0
  155. package/lib/amplify-outputs-guard.ts +24 -0
  156. package/lib/amplify-outputs-path.ts +16 -0
  157. package/lib/amplify-server-runtime.ts +63 -0
  158. package/lib/archive-data.ts +30 -0
  159. package/lib/archive-types.ts +15 -0
  160. package/lib/articles.ts +320 -0
  161. package/lib/cached-content-repository.ts +89 -0
  162. package/lib/category-dashboard.ts +37 -0
  163. package/lib/category-repository.ts +1907 -0
  164. package/lib/console-chat-client.ts +1316 -0
  165. package/lib/console-web-context.ts +118 -0
  166. package/lib/content-load-timing.ts +23 -0
  167. package/lib/content-repository.ts +40 -0
  168. package/lib/content-types.ts +104 -0
  169. package/lib/define-site.js +3 -0
  170. package/lib/define-site.ts +21 -0
  171. package/lib/demo-amplify-outputs.ts +15 -0
  172. package/lib/dev-sandbox-editor-auth.ts +18 -0
  173. package/lib/doctrine.ts +128 -0
  174. package/lib/edition-routes.ts +155 -0
  175. package/lib/edition-sections.ts +111 -0
  176. package/lib/empty-edition-layout-plan.ts +109 -0
  177. package/lib/excerpts.ts +8 -0
  178. package/lib/graphql-content-repository.ts +986 -0
  179. package/lib/image-url.ts +10 -0
  180. package/lib/layout-plan.ts +981 -0
  181. package/lib/layout-scenarios.ts +564 -0
  182. package/lib/markus-body.ts +168 -0
  183. package/lib/markus-ir.ts +235 -0
  184. package/lib/markus-projection.ts +243 -0
  185. package/lib/markus-to-article.ts +93 -0
  186. package/lib/news-desk-session.ts +160 -0
  187. package/lib/newspaper-layout.ts +3459 -0
  188. package/lib/newsroom-assignments.ts +173 -0
  189. package/lib/newsroom-base-path.ts +48 -0
  190. package/lib/newsroom-card-layout.ts +102 -0
  191. package/lib/newsroom-category-drilldown.ts +133 -0
  192. package/lib/newsroom-demo-dashboard.ts +267 -0
  193. package/lib/newsroom-demo-profile.ts +46 -0
  194. package/lib/newsroom-forum-routes.ts +153 -0
  195. package/lib/newsroom-index-filters.ts +202 -0
  196. package/lib/newsroom-knowledge-query-request.ts +51 -0
  197. package/lib/newsroom-list-selection.ts +42 -0
  198. package/lib/newsroom-nav.ts +42 -0
  199. package/lib/newsroom-references.ts +116 -0
  200. package/lib/newsroom-sections.ts +52 -0
  201. package/lib/newsroom-topics.ts +118 -0
  202. package/lib/papyrus-object-kinds.ts +24 -0
  203. package/lib/papyrus-web-locations.ts +480 -0
  204. package/lib/pilobol-sample.ts +167 -0
  205. package/lib/presentation-footer.ts +92 -0
  206. package/lib/pretext-layout.ts +179 -0
  207. package/lib/public-placeholder-config.ts +102 -0
  208. package/lib/publication-items.ts +181 -0
  209. package/lib/reader-route-config.ts +1 -0
  210. package/lib/reader-static-params.ts +56 -0
  211. package/lib/reader-storage-url.ts +63 -0
  212. package/lib/reference-display.ts +74 -0
  213. package/lib/reference-policy.ts +97 -0
  214. package/lib/reference-source-preview.ts +175 -0
  215. package/lib/renderer-config.ts +50 -0
  216. package/lib/renderer.ts +87 -0
  217. package/lib/reporting-story-budget.ts +638 -0
  218. package/lib/semantic-graph.ts +554 -0
  219. package/lib/site-brand-tenant.ts +41 -0
  220. package/lib/site-brand.ts +179 -0
  221. package/lib/site-renderer.ts +17 -0
  222. package/lib/site-stack.ts +195 -0
  223. package/lib/themed-image.ts +30 -0
  224. package/lib/utils.ts +6 -0
  225. package/middleware.ts +78 -0
  226. package/package.json +296 -1
  227. package/publications/anth_us/brand.ts +68 -0
  228. package/publications/anth_us/theme.css +66 -0
  229. package/publications/papyrus/theme.css +11 -0
  230. package/publications/pilobol_us/brand.ts +37 -0
  231. package/publications/pilobol_us/theme.css +140 -0
  232. package/publications/threat_intelligence/blog-defense/graph.ts +636 -0
  233. package/publications/threat_intelligence/blog-defense/layout.ts +934 -0
  234. package/publications/threat_intelligence/blog-defense/page-background.tsx +590 -0
  235. package/publications/threat_intelligence/brand.ts +40 -0
  236. package/publications/threat_intelligence/pictograms/art.tsx +608 -0
  237. package/publications/threat_intelligence/pictograms/figure.tsx +82 -0
  238. package/publications/threat_intelligence/pictograms/registry.ts +30 -0
  239. package/publications/threat_intelligence/pictograms/system.tsx +305 -0
  240. package/publications/threat_intelligence/seed/seed-edition-content.json +629 -0
  241. package/publications/threat_intelligence/theme.css +1440 -0
  242. package/renderers/markus/index.ts +25 -0
  243. package/renderers/markus/stubs.tsx +33 -0
  244. package/renderers/pretext/article-page.tsx +174 -0
  245. package/renderers/pretext/index.ts +30 -0
  246. package/renderers/pretext/layouts.ts +16 -0
  247. package/renderers/pretext/presentation-shell.tsx +477 -0
  248. package/routes.manifest.json +182 -0
  249. package/scripts/fixtures/pilobol-sample.ast.json +234 -0
  250. package/src/backend.js +1 -0
  251. package/src/backend.ts +3 -0
  252. package/src/sync.mjs +87 -0
  253. package/src/with-papyrus.d.mts +2 -0
  254. package/src/with-papyrus.mjs +47 -0
@@ -0,0 +1,92 @@
1
+ import { getEditionSectionItems } from "./edition-sections";
2
+ import type { EditionContent } from "./content-types";
3
+ import type { PublicationItem } from "./publication-items";
4
+
5
+ export type PresentationFooterEntry = {
6
+ section: string;
7
+ sectionKey: string;
8
+ articleSlug: string;
9
+ articleTitle: string;
10
+ };
11
+
12
+ export type PresentationFooterUtilityEntry =
13
+ | {
14
+ id: "archive";
15
+ label: string;
16
+ href: string;
17
+ disabled: false;
18
+ }
19
+ | {
20
+ id: "newsDesk";
21
+ label: string;
22
+ href: string;
23
+ disabled: false;
24
+ }
25
+ | {
26
+ id: "settings";
27
+ label: string;
28
+ href: string;
29
+ disabled: false;
30
+ }
31
+ | {
32
+ id: "login";
33
+ label: string;
34
+ disabled: true;
35
+ };
36
+
37
+ export type PresentationFooterGeometry = {
38
+ height: number;
39
+ marginTop: number;
40
+ rowHeight: number;
41
+ sectionColumns: number;
42
+ sectionRows: number;
43
+ };
44
+
45
+ export const PRESENTATION_FOOTER_UTILITIES: PresentationFooterUtilityEntry[] = [
46
+ { id: "archive", label: "Archive", href: "/archive", disabled: false },
47
+ { id: "newsDesk", label: "Newsroom", href: "/newsroom", disabled: false },
48
+ { id: "settings", label: "Settings", href: "/settings", disabled: false },
49
+ { id: "login", label: "LOGIN", disabled: true },
50
+ ];
51
+
52
+ export function buildPresentationFooterEntries(content: EditionContent): PresentationFooterEntry[] {
53
+ const entries: PresentationFooterEntry[] = [];
54
+ const seenSections = new Set<string>();
55
+
56
+ for (const section of content.sections) {
57
+ const sectionKey = section.label.trim().toLowerCase();
58
+ if (!sectionKey || seenSections.has(sectionKey)) continue;
59
+
60
+ const leadItem = getEditionSectionItems(section, content.items).find((item) => isFooterEligibleItem(item));
61
+ if (!leadItem) continue;
62
+
63
+ seenSections.add(sectionKey);
64
+ entries.push({
65
+ section: section.label,
66
+ sectionKey: section.key,
67
+ articleSlug: leadItem.slug,
68
+ articleTitle: getPresentationFooterItemTitle(leadItem),
69
+ });
70
+ }
71
+
72
+ return entries;
73
+ }
74
+
75
+ export function getPresentationFooterGrid(entryCount: number, maxColumns = 4): Pick<PresentationFooterGeometry, "sectionColumns" | "sectionRows"> {
76
+ const sectionColumns = entryCount === 0 ? 1 : Math.min(entryCount, Math.max(1, maxColumns));
77
+ const sectionRows = entryCount === 0 ? 0 : Math.ceil(entryCount / sectionColumns);
78
+ return { sectionColumns, sectionRows };
79
+ }
80
+
81
+ export function getPresentationFooterSubtitle(content: EditionContent): string {
82
+ const description = content.description?.trim();
83
+ return description && description.length > 0 ? description : "Inside Papyrus";
84
+ }
85
+
86
+ function isFooterEligibleItem(item: PublicationItem): boolean {
87
+ return item.type === "article" || item.type === "brief";
88
+ }
89
+
90
+ function getPresentationFooterItemTitle(item: PublicationItem): string {
91
+ return item.type === "article" ? item.headline : item.title;
92
+ }
@@ -0,0 +1,179 @@
1
+ import {
2
+ layoutNextLine,
3
+ prepareWithSegments,
4
+ type LayoutCursor,
5
+ type LayoutLine,
6
+ type PreparedTextWithSegments,
7
+ } from "@chenglou/pretext";
8
+
9
+ export { layoutNextLine, prepareWithSegments, type LayoutCursor, type PreparedTextWithSegments };
10
+
11
+ export type TextLine = {
12
+ text: string;
13
+ width: number;
14
+ x: number;
15
+ y: number;
16
+ fontSize: number;
17
+ fontFamily: string;
18
+ lineHeight: number;
19
+ paintHeight: number;
20
+ };
21
+
22
+ export type TextObstacle = {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ };
28
+
29
+ export type LayoutTextLinesOptions = {
30
+ prepared: PreparedTextWithSegments;
31
+ cursor: LayoutCursor;
32
+ maxHeight: number;
33
+ maxWidth: number;
34
+ lineHeight: number;
35
+ linePaintHeight: number;
36
+ fontSize: number;
37
+ fontFamily: string;
38
+ obstacles: TextObstacle[];
39
+ };
40
+
41
+ export function layoutTextLines({
42
+ prepared,
43
+ cursor,
44
+ maxHeight,
45
+ maxWidth,
46
+ lineHeight,
47
+ linePaintHeight,
48
+ fontSize,
49
+ fontFamily,
50
+ obstacles,
51
+ }: LayoutTextLinesOptions): { lines: TextLine[]; cursor: LayoutCursor; hasMore: boolean } {
52
+ const lines: TextLine[] = [];
53
+ let current = { ...cursor };
54
+ const maxLines = getVisibleLineCapacity(maxHeight, lineHeight, linePaintHeight);
55
+
56
+ for (let lineIndex = 0; lineIndex < maxLines; lineIndex += 1) {
57
+ const y = lineIndex * lineHeight;
58
+ const slot = getAvailableSlot(maxWidth, y, linePaintHeight, obstacles);
59
+ if (!slot) continue;
60
+ const line = layoutNextLine(prepared, current, slot.width);
61
+ if (!line) return { lines, cursor: current, hasMore: false };
62
+ lines.push(toTextLine(line, slot.x, y, fontSize, fontFamily, lineHeight, linePaintHeight));
63
+ current = line.end;
64
+ }
65
+
66
+ const nextSlot = getNextAvailableSlot(maxWidth, maxLines, lineHeight, linePaintHeight, obstacles) ?? { x: 0, width: maxWidth };
67
+ return {
68
+ lines,
69
+ cursor: current,
70
+ hasMore: layoutNextLine(prepared, current, nextSlot.width) !== null,
71
+ };
72
+ }
73
+
74
+ export function layoutAllTextLines({
75
+ prepared,
76
+ maxWidth,
77
+ lineHeight,
78
+ linePaintHeight,
79
+ fontSize,
80
+ fontFamily,
81
+ }: Omit<LayoutTextLinesOptions, "cursor" | "maxHeight" | "obstacles">): TextLine[] {
82
+ const lines: TextLine[] = [];
83
+ let cursor: LayoutCursor = { segmentIndex: 0, graphemeIndex: 0 };
84
+
85
+ for (let lineIndex = 0; lineIndex < 2000; lineIndex += 1) {
86
+ const line = layoutNextLine(prepared, cursor, maxWidth);
87
+ if (!line) return lines;
88
+ lines.push(toTextLine(line, 0, lineIndex * lineHeight, fontSize, fontFamily, lineHeight, linePaintHeight));
89
+ cursor = line.end;
90
+ }
91
+
92
+ return lines;
93
+ }
94
+
95
+ function getVisibleLineCapacity(maxHeight: number, lineHeight: number, linePaintHeight: number): number {
96
+ if (maxHeight < linePaintHeight) return 0;
97
+ return Math.floor((maxHeight - linePaintHeight) / lineHeight) + 1;
98
+ }
99
+
100
+ function getNextAvailableSlot(
101
+ maxWidth: number,
102
+ startLineIndex: number,
103
+ lineHeight: number,
104
+ linePaintHeight: number,
105
+ obstacles: TextObstacle[],
106
+ ): { x: number; width: number } | null {
107
+ for (let offset = 0; offset < 24; offset += 1) {
108
+ const slot = getAvailableSlot(maxWidth, (startLineIndex + offset) * lineHeight, linePaintHeight, obstacles);
109
+ if (slot) return slot;
110
+ }
111
+ return null;
112
+ }
113
+
114
+ function getAvailableSlot(
115
+ maxWidth: number,
116
+ y: number,
117
+ lineHeight: number,
118
+ obstacles: TextObstacle[],
119
+ ): { x: number; width: number } | null {
120
+ if (obstacles.length === 0) return { x: 0, width: maxWidth };
121
+ const bandTop = y;
122
+ const bandBottom = y + lineHeight;
123
+ const gutter = 14;
124
+ const intervals = obstacles
125
+ .filter((obstacle) => bandBottom > obstacle.y && bandTop < obstacle.y + obstacle.height)
126
+ .map((obstacle) => ({
127
+ start: clamp(obstacle.x - gutter, 0, maxWidth),
128
+ end: clamp(obstacle.x + obstacle.width + gutter, 0, maxWidth),
129
+ }))
130
+ .filter((interval) => interval.end > interval.start)
131
+ .sort((a, b) => a.start - b.start);
132
+
133
+ if (intervals.length === 0) return { x: 0, width: maxWidth };
134
+
135
+ const merged: Array<{ start: number; end: number }> = [];
136
+ for (const interval of intervals) {
137
+ const previous = merged[merged.length - 1];
138
+ if (previous && interval.start <= previous.end) {
139
+ previous.end = Math.max(previous.end, interval.end);
140
+ } else {
141
+ merged.push({ ...interval });
142
+ }
143
+ }
144
+
145
+ const gaps: Array<{ x: number; width: number }> = [];
146
+ let cursorX = 0;
147
+ for (const interval of merged) {
148
+ if (interval.start > cursorX) {
149
+ gaps.push({ x: cursorX, width: interval.start - cursorX });
150
+ }
151
+ cursorX = Math.max(cursorX, interval.end);
152
+ }
153
+ if (cursorX < maxWidth) {
154
+ gaps.push({ x: cursorX, width: maxWidth - cursorX });
155
+ }
156
+
157
+ const slot = gaps.reduce<{ x: number; width: number } | null>(
158
+ (best, gap) => (!best || gap.width > best.width ? gap : best),
159
+ null,
160
+ );
161
+ return slot && slot.width >= 80 ? slot : null;
162
+ }
163
+
164
+ function toTextLine(line: LayoutLine, x: number, y: number, fontSize: number, fontFamily: string, lineHeight: number, paintHeight: number): TextLine {
165
+ return {
166
+ text: line.text,
167
+ width: line.width,
168
+ x,
169
+ y,
170
+ fontSize,
171
+ fontFamily,
172
+ lineHeight,
173
+ paintHeight,
174
+ };
175
+ }
176
+
177
+ function clamp(value: number, min: number, max: number): number {
178
+ return Math.max(min, Math.min(max, value));
179
+ }
@@ -0,0 +1,102 @@
1
+ import "server-only";
2
+
3
+ import fs from "node:fs";
4
+ import path from "node:path";
5
+ import { parse } from "yaml";
6
+
7
+ export type PublicPlaceholderSectionType = "canonical" | "floating";
8
+
9
+ export type PublicPlaceholderSection = {
10
+ id: string;
11
+ title: string;
12
+ shortTitle: string;
13
+ type: PublicPlaceholderSectionType;
14
+ sortOrder: number;
15
+ };
16
+
17
+ export type PublicPlaceholderTopic = {
18
+ key: string;
19
+ title: string;
20
+ shortTitle: string;
21
+ description: string;
22
+ rank: number;
23
+ };
24
+
25
+ const NEWSROOM_SECTIONS_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-newsroom-sections.yml");
26
+ const PUBLIC_TOPICS_CONFIG_PATH = path.join(process.cwd(), "corpora", "papyrus-public-topics.yml");
27
+
28
+ export function loadPublicPlaceholderSections(): PublicPlaceholderSection[] {
29
+ const parsed = readYamlRecord(NEWSROOM_SECTIONS_CONFIG_PATH);
30
+ const sections = Array.isArray(parsed.sections) ? parsed.sections : [];
31
+ return sections
32
+ .map((entry, index) => normalizePublicSection(entry, index))
33
+ .filter((section): section is PublicPlaceholderSection => section !== null)
34
+ .sort((left, right) => left.sortOrder - right.sortOrder || left.title.localeCompare(right.title));
35
+ }
36
+
37
+ export function loadPublicPlaceholderTopics(): PublicPlaceholderTopic[] {
38
+ const parsed = readYamlRecord(PUBLIC_TOPICS_CONFIG_PATH);
39
+ const topics = Array.isArray(parsed.topics) ? parsed.topics : [];
40
+ return topics
41
+ .map((entry, index) => normalizePublicTopic(entry, index))
42
+ .sort((left, right) => left.rank - right.rank || left.title.localeCompare(right.title));
43
+ }
44
+
45
+ function readYamlRecord(filePath: string): Record<string, unknown> {
46
+ const parsed = parse(fs.readFileSync(filePath, "utf8"));
47
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
48
+ throw new Error(`Invalid public placeholder config: ${filePath}`);
49
+ }
50
+ return parsed as Record<string, unknown>;
51
+ }
52
+
53
+ function normalizePublicSection(entry: unknown, index: number): PublicPlaceholderSection | null {
54
+ const record = objectRecord(entry);
55
+ const enabled = record.enabled;
56
+ if (enabled === false) return null;
57
+
58
+ const id = requiredString(record.id, `sections[${index}].id`);
59
+ const title = requiredString(record.title, `sections[${index}].title`);
60
+ const shortTitle = requiredString(record.shortTitle, `sections[${index}].shortTitle`);
61
+ const rawType = requiredString(record.type, `sections[${index}].type`);
62
+ const type = rawType === "canonical" ? "canonical" : rawType === "floating" || rawType === "rotating" ? "floating" : null;
63
+ if (!type) throw new Error(`Unsupported public section type '${rawType}' for ${id}`);
64
+
65
+ return {
66
+ id,
67
+ title,
68
+ shortTitle,
69
+ type,
70
+ sortOrder: numberValue(record.sortOrder) ?? index + 1,
71
+ };
72
+ }
73
+
74
+ function normalizePublicTopic(entry: unknown, index: number): PublicPlaceholderTopic {
75
+ const record = objectRecord(entry);
76
+ return {
77
+ key: requiredString(record.key, `topics[${index}].key`),
78
+ title: requiredString(record.title, `topics[${index}].title`),
79
+ shortTitle: requiredString(record.shortTitle, `topics[${index}].shortTitle`),
80
+ description: requiredString(record.description, `topics[${index}].description`),
81
+ rank: numberValue(record.rank) ?? index + 1,
82
+ };
83
+ }
84
+
85
+ function objectRecord(value: unknown): Record<string, unknown> {
86
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
87
+ return value as Record<string, unknown>;
88
+ }
89
+
90
+ function requiredString(value: unknown, label: string): string {
91
+ if (typeof value === "string" && value.trim()) return value.trim();
92
+ throw new Error(`Missing required public placeholder value: ${label}`);
93
+ }
94
+
95
+ function numberValue(value: unknown): number | null {
96
+ if (typeof value === "number" && Number.isFinite(value)) return value;
97
+ if (typeof value === "string" && value.trim()) {
98
+ const parsed = Number(value);
99
+ return Number.isFinite(parsed) ? parsed : null;
100
+ }
101
+ return null;
102
+ }
@@ -0,0 +1,181 @@
1
+ import {
2
+ type Article,
3
+ type ArticleAsset,
4
+ type ArticleImage,
5
+ type ArticleImageAsset,
6
+ type ArticleVideoAsset,
7
+ getArticleImageAssets,
8
+ getArticleText,
9
+ getArticleVideoAsset,
10
+ } from "./articles";
11
+
12
+ export type PublicationItemType = "article" | "brief" | "correction" | "promo" | "ad" | "sectionHeader";
13
+
14
+ export type ArticlePublicationItem = Article & {
15
+ type: "article";
16
+ };
17
+
18
+ export type NonArticlePublicationItem = {
19
+ type: Exclude<PublicationItemType, "article">;
20
+ slug: string;
21
+ section?: string;
22
+ title: string;
23
+ deck?: string;
24
+ excerpt?: string;
25
+ body?: string[];
26
+ image?: ArticleImage;
27
+ video?: ArticleVideoAsset;
28
+ assets?: ArticleAsset[];
29
+ href?: string;
30
+ metadata?: Record<string, unknown>;
31
+ };
32
+
33
+ export type PublicationItem = ArticlePublicationItem | NonArticlePublicationItem;
34
+
35
+ export function articleToPublicationItem(article: Article): ArticlePublicationItem {
36
+ return {
37
+ ...cloneArticle(article),
38
+ type: "article",
39
+ };
40
+ }
41
+
42
+ export function publicationItemToArticle(item: PublicationItem): Article | undefined {
43
+ if (item.type !== "article") return undefined;
44
+ const { type: _type, ...article } = item;
45
+ return cloneArticle(article);
46
+ }
47
+
48
+ export function getArticlePublicationItems(items: PublicationItem[]): ArticlePublicationItem[] {
49
+ return items.filter((item): item is ArticlePublicationItem => item.type === "article");
50
+ }
51
+
52
+ export function getPublicationItemText(item: PublicationItem): string {
53
+ if (item.type === "article") return getArticleText(item);
54
+ return [item.title, item.deck, ...(item.body ?? [])].filter(Boolean).join("\n\n");
55
+ }
56
+
57
+ export function getPublicationItemImageAssets(item: PublicationItem): ArticleImageAsset[] {
58
+ if (item.type === "article") return getArticleImageAssets(item);
59
+ const explicitAssets = item.assets?.filter((asset): asset is ArticleImageAsset => asset.type === "image") ?? [];
60
+ if (explicitAssets.length > 0) return explicitAssets;
61
+ if (!item.image) return [];
62
+ return [
63
+ {
64
+ ...item.image,
65
+ id: `${item.slug}-primary-image`,
66
+ type: "image",
67
+ roles: ["feature"],
68
+ },
69
+ ];
70
+ }
71
+
72
+ export function getPublicationItemVideoAsset(item: PublicationItem): ArticleVideoAsset | undefined {
73
+ if (item.type === "article") return getArticleVideoAsset(item);
74
+ const explicitAssets = item.assets?.filter((asset): asset is ArticleVideoAsset => asset.type === "video") ?? [];
75
+ if (explicitAssets.length > 0) {
76
+ return explicitAssets.find((asset) => asset.roles?.includes("lead")) ?? explicitAssets[0];
77
+ }
78
+ return item.video;
79
+ }
80
+
81
+ export function clonePublicationItems(items: PublicationItem[]): PublicationItem[] {
82
+ return items.map((item) => (item.type === "article" ? articleToPublicationItem(item) : cloneNonArticleItem(item)));
83
+ }
84
+
85
+ export function cloneArticle(article: Article): Article {
86
+ return {
87
+ ...article,
88
+ image: article.image
89
+ ? {
90
+ ...article.image,
91
+ layout: cloneImageLayout(article.image.layout),
92
+ themeVariants: cloneImageThemeVariants(article.image.themeVariants),
93
+ }
94
+ : undefined,
95
+ video: article.video
96
+ ? {
97
+ ...article.video,
98
+ roles: article.video.roles ? [...article.video.roles] : undefined,
99
+ themeVariants: cloneVideoThemeVariants(article.video.themeVariants),
100
+ }
101
+ : undefined,
102
+ assets: article.assets?.map((asset) =>
103
+ asset.type === "video"
104
+ ? {
105
+ ...asset,
106
+ roles: asset.roles ? [...asset.roles] : undefined,
107
+ themeVariants: cloneVideoThemeVariants(asset.themeVariants),
108
+ }
109
+ : {
110
+ ...asset,
111
+ layout: cloneImageLayout(asset.layout),
112
+ roles: asset.roles ? [...asset.roles] : undefined,
113
+ themeVariants: cloneImageThemeVariants(asset.themeVariants),
114
+ },
115
+ ),
116
+ pullQuotes: article.pullQuotes ? [...article.pullQuotes] : undefined,
117
+ body: [...article.body],
118
+ };
119
+ }
120
+
121
+ function cloneNonArticleItem(item: NonArticlePublicationItem): NonArticlePublicationItem {
122
+ return {
123
+ ...item,
124
+ image: item.image
125
+ ? {
126
+ ...item.image,
127
+ layout: cloneImageLayout(item.image.layout),
128
+ themeVariants: cloneImageThemeVariants(item.image.themeVariants),
129
+ }
130
+ : undefined,
131
+ video: item.video
132
+ ? {
133
+ ...item.video,
134
+ roles: item.video.roles ? [...item.video.roles] : undefined,
135
+ themeVariants: cloneVideoThemeVariants(item.video.themeVariants),
136
+ }
137
+ : undefined,
138
+ assets: item.assets?.map((asset) =>
139
+ asset.type === "video"
140
+ ? {
141
+ ...asset,
142
+ roles: asset.roles ? [...asset.roles] : undefined,
143
+ themeVariants: cloneVideoThemeVariants(asset.themeVariants),
144
+ }
145
+ : {
146
+ ...asset,
147
+ layout: cloneImageLayout(asset.layout),
148
+ roles: asset.roles ? [...asset.roles] : undefined,
149
+ themeVariants: cloneImageThemeVariants(asset.themeVariants),
150
+ },
151
+ ),
152
+ body: item.body ? [...item.body] : undefined,
153
+ metadata: item.metadata ? { ...item.metadata } : undefined,
154
+ };
155
+ }
156
+
157
+ function cloneImageLayout<T extends ArticleImage["layout"]>(layout: T): T {
158
+ if (!layout) return undefined as T;
159
+ return {
160
+ ...layout,
161
+ inlineFloat: layout.inlineFloat ? { ...layout.inlineFloat } : undefined,
162
+ focalPoint: layout.focalPoint ? { ...layout.focalPoint } : undefined,
163
+ } as T;
164
+ }
165
+
166
+ function cloneImageThemeVariants<T extends ArticleImage["themeVariants"]>(themeVariants: T): T {
167
+ if (!themeVariants) return undefined as T;
168
+ return {
169
+ ...themeVariants,
170
+ dark: themeVariants.dark ? { ...themeVariants.dark } : undefined,
171
+ } as T;
172
+ }
173
+
174
+ function cloneVideoThemeVariants<T extends ArticleVideoAsset["themeVariants"]>(themeVariants: T): T {
175
+ if (!themeVariants) return undefined as T;
176
+ return {
177
+ ...themeVariants,
178
+ light: themeVariants.light ? { ...themeVariants.light } : undefined,
179
+ dark: themeVariants.dark ? { ...themeVariants.dark } : undefined,
180
+ } as T;
181
+ }
@@ -0,0 +1 @@
1
+ export const READER_REVALIDATE_SECONDS = 3600;
@@ -0,0 +1,56 @@
1
+ import { graphqlContentRepository } from "./graphql-content-repository";
2
+
3
+ const MONTH_NAMES = [
4
+ "january",
5
+ "february",
6
+ "march",
7
+ "april",
8
+ "may",
9
+ "june",
10
+ "july",
11
+ "august",
12
+ "september",
13
+ "october",
14
+ "november",
15
+ "december",
16
+ ] as const;
17
+
18
+ type EditionDateStaticParam = {
19
+ year: string;
20
+ month: string;
21
+ day: string;
22
+ };
23
+
24
+ export async function generateEditionDateStaticParams(): Promise<EditionDateStaticParam[]> {
25
+ if (process.env.NODE_ENV === "development") return [];
26
+
27
+ const { editions } = await graphqlContentRepository.listPublishedEditions({ limit: 100 });
28
+ const params: EditionDateStaticParam[] = [];
29
+ for (const edition of editions) {
30
+ const route = editionDateToStaticParam(edition.editionDate);
31
+ if (route) params.push(route);
32
+ }
33
+ return params;
34
+ }
35
+
36
+ export async function generateArticleStaticParams(): Promise<Array<{ slug: string }>> {
37
+ if (process.env.NODE_ENV === "development") return [];
38
+
39
+ const slugs = await graphqlContentRepository.listArticleSlugs();
40
+ return slugs.map((slug) => ({ slug }));
41
+ }
42
+
43
+ function editionDateToStaticParam(editionDate: string): EditionDateStaticParam | null {
44
+ const match = editionDate.match(/^(\d{4})-(\d{2})-(\d{2})$/);
45
+ if (!match) return null;
46
+
47
+ const monthIndex = Number(match[2]) - 1;
48
+ const monthName = MONTH_NAMES[monthIndex];
49
+ if (!monthName) return null;
50
+
51
+ return {
52
+ year: match[1],
53
+ month: monthName,
54
+ day: String(Number(match[3])),
55
+ };
56
+ }
@@ -0,0 +1,63 @@
1
+ import { getUrl } from "aws-amplify/storage/server";
2
+ import { getAmplifyServerRuntime } from "./amplify-server-runtime";
3
+
4
+ const STORAGE_URL_EXPIRES_IN_SECONDS = 60 * 60;
5
+ const STORAGE_URL_CACHE_TTL_MS = 55 * 60 * 1000;
6
+ const GUEST_READ_PREFIX = "media/";
7
+
8
+ type SignedUrlCacheEntry = {
9
+ expiresAt: number;
10
+ url: string;
11
+ };
12
+
13
+ const signedUrlCache = new Map<string, SignedUrlCacheEntry>();
14
+
15
+ export function isGuestReadableStoragePath(storagePath: string): boolean {
16
+ const normalized = storagePath.trim().replace(/^\/+/, "");
17
+ return normalized.startsWith(GUEST_READ_PREFIX);
18
+ }
19
+
20
+ export function buildReaderMediaProxyUrl(storagePath: string): string {
21
+ const normalized = storagePath.trim().replace(/^\/+/, "");
22
+ if (!isGuestReadableStoragePath(normalized)) {
23
+ throw new Error(`Storage path is not reader-public: ${storagePath}`);
24
+ }
25
+ return `/api/media/${normalized.split("/").map((segment) => encodeURIComponent(segment)).join("/")}`;
26
+ }
27
+
28
+ export async function resolveReaderStorageUrl(storagePath: string): Promise<string> {
29
+ const normalized = storagePath.trim().replace(/^\/+/, "");
30
+ if (!normalized) {
31
+ throw new Error("Missing storage path.");
32
+ }
33
+ if (isGuestReadableStoragePath(normalized)) {
34
+ return buildReaderMediaProxyUrl(normalized);
35
+ }
36
+ return signStorageUrl(normalized);
37
+ }
38
+
39
+ export async function signStorageUrl(storagePath: string): Promise<string> {
40
+ const normalized = storagePath.trim().replace(/^\/+/, "");
41
+ const cached = signedUrlCache.get(normalized);
42
+ if (cached && cached.expiresAt > Date.now()) {
43
+ return cached.url;
44
+ }
45
+
46
+ const { runWithAmplifyServerContext } = getAmplifyServerRuntime();
47
+ const result = await runWithAmplifyServerContext({
48
+ nextServerContext: null,
49
+ operation: (contextSpec) =>
50
+ getUrl(contextSpec, {
51
+ path: normalized,
52
+ options: {
53
+ expiresIn: STORAGE_URL_EXPIRES_IN_SECONDS,
54
+ },
55
+ }),
56
+ });
57
+ const url = result.url.toString();
58
+ signedUrlCache.set(normalized, {
59
+ expiresAt: Date.now() + STORAGE_URL_CACHE_TTL_MS,
60
+ url,
61
+ });
62
+ return url;
63
+ }