@anthusai/papyrus 0.0.0 → 1.0.0-next.10
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/LICENSE +21 -0
- package/amplify/auth/resource.js +61 -0
- package/amplify/auth/resource.ts +65 -0
- package/amplify/data/resource.js +2069 -0
- package/amplify/data/resource.ts +2185 -0
- package/amplify/data/schema.js +1882 -0
- package/amplify/data/schema.ts +1990 -0
- package/amplify/functions/assignment-action/handler.ts +1416 -0
- package/amplify/functions/assignment-action/resource.js +8 -0
- package/amplify/functions/assignment-action/resource.ts +9 -0
- package/amplify/functions/category-action/handler.ts +2259 -0
- package/amplify/functions/category-action/resource.js +8 -0
- package/amplify/functions/category-action/resource.ts +9 -0
- package/amplify/functions/console-chat-responder/Cargo.lock +3456 -0
- package/amplify/functions/console-chat-responder/Cargo.toml +26 -0
- package/amplify/functions/console-chat-responder/Dockerfile +19 -0
- package/amplify/functions/console-chat-responder/py/execute_tactus_runner.py +116 -0
- package/amplify/functions/console-chat-responder/resource.js +153 -0
- package/amplify/functions/console-chat-responder/resource.ts +179 -0
- package/amplify/functions/console-chat-responder/src/main.rs +3242 -0
- package/amplify/functions/content-actions/handler.py +149 -0
- package/amplify/functions/content-actions/requirements.txt +4 -0
- package/amplify/functions/content-actions/resource.js +40 -0
- package/amplify/functions/content-actions/resource.ts +54 -0
- package/amplify/functions/email-submission-processor/handler.py +46 -0
- package/amplify/functions/email-submission-processor/requirements.txt +2 -0
- package/amplify/functions/email-submission-processor/resource.js +56 -0
- package/amplify/functions/email-submission-processor/resource.ts +69 -0
- package/amplify/functions/graphql-jwt-authorizer/handler.ts +193 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.js +14 -0
- package/amplify/functions/graphql-jwt-authorizer/resource.ts +15 -0
- package/amplify/functions/knowledge-query/handler.py +1 -0
- package/amplify/functions/knowledge-query/requirements.txt +3 -0
- package/amplify/functions/knowledge-query/resource.js +50 -0
- package/amplify/functions/knowledge-query/resource.ts +61 -0
- package/amplify/functions/manage-user-role/handler.ts +799 -0
- package/amplify/functions/manage-user-role/resource.js +8 -0
- package/amplify/functions/manage-user-role/resource.ts +9 -0
- package/amplify/functions/model-attachment-upload/handler.ts +387 -0
- package/amplify/functions/model-attachment-upload/resource.js +8 -0
- package/amplify/functions/model-attachment-upload/resource.ts +9 -0
- package/amplify/functions/newsroom-summary/handler.ts +321 -0
- package/amplify/functions/newsroom-summary/resource.js +8 -0
- package/amplify/functions/newsroom-summary/resource.ts +9 -0
- package/amplify/functions/procedure-action/handler.ts +621 -0
- package/amplify/functions/procedure-action/resource.js +8 -0
- package/amplify/functions/procedure-action/resource.ts +9 -0
- package/amplify/functions/reader-settings/handler.ts +322 -0
- package/amplify/functions/reader-settings/resource.js +8 -0
- package/amplify/functions/reader-settings/resource.ts +9 -0
- package/amplify/functions/ses-inbound-receive/handler.ts +493 -0
- package/amplify/functions/ses-inbound-receive/resource.js +8 -0
- package/amplify/functions/ses-inbound-receive/resource.ts +9 -0
- package/amplify/functions/shared/amplify-secrets.ts +65 -0
- package/amplify/functions/shared/console-chat-enqueue.ts +117 -0
- package/amplify/functions/shared/email-mime-intake.ts +196 -0
- package/amplify/functions/shared/email-submission.ts +415 -0
- package/amplify/functions/shared/inbound-email-intake.ts +37 -0
- package/amplify/functions/shared/lambda-data-client.ts +21 -0
- package/amplify/functions/shared/model-payloads.ts +186 -0
- package/amplify/functions/shared/python-bundle.js +130 -0
- package/amplify/functions/shared/python-bundle.ts +162 -0
- package/amplify/functions/shared/slack-events.ts +111 -0
- package/amplify/functions/slack-delivery/handler.py +112 -0
- package/amplify/functions/slack-delivery/requirements.txt +2 -0
- package/amplify/functions/slack-delivery/resource.js +38 -0
- package/amplify/functions/slack-delivery/resource.ts +51 -0
- package/amplify/functions/slack-events/handler.ts +155 -0
- package/amplify/functions/slack-events/resource.js +11 -0
- package/amplify/functions/slack-events/resource.ts +12 -0
- package/amplify/inbound-email/stack.js +46 -0
- package/amplify/inbound-email/stack.ts +61 -0
- package/amplify/package.json +3 -0
- package/amplify/rebuild-trigger-settings.js +22 -0
- package/amplify/rebuild-trigger-settings.ts +42 -0
- package/amplify/site-backend-config.js +1 -0
- package/amplify/site-backend-config.ts +36 -0
- package/amplify/site-backend.js +615 -0
- package/amplify/site-backend.ts +739 -0
- package/amplify/storage/resource.js +21 -0
- package/amplify/storage/resource.ts +22 -0
- package/app/%5F_preview/[[...path]]/route.ts +8 -0
- package/app/[year]/[month]/[day]/[articleSlug]/page.tsx +62 -0
- package/app/[year]/[month]/[day]/edition-route-page.tsx +53 -0
- package/app/[year]/[month]/[day]/page/[pageNumber]/page.tsx +32 -0
- package/app/[year]/[month]/[day]/page.tsx +22 -0
- package/app/[year]/[month]/[day]/section/[sectionKey]/page.tsx +32 -0
- package/app/api/archive/editions/route.ts +18 -0
- package/app/api/media/[...path]/route.ts +35 -0
- package/app/api/revalidate/route.ts +94 -0
- package/app/archive/page.tsx +24 -0
- package/app/articles/[slug]/page.tsx +35 -0
- package/app/globals.css +10345 -0
- package/app/layout.tsx +142 -0
- package/app/newsroom/[section]/[...selection]/page.tsx +50 -0
- package/app/newsroom/[section]/page.tsx +47 -0
- package/app/newsroom/articles/[id]/page.tsx +15 -0
- package/app/newsroom/articles/page.tsx +13 -0
- package/app/newsroom/forum/[threadId]/page.tsx +35 -0
- package/app/newsroom/insights/[threadId]/page.tsx +22 -0
- package/app/newsroom/insights/page.tsx +14 -0
- package/app/newsroom/layout.tsx +12 -0
- package/app/newsroom/page.tsx +31 -0
- package/app/newsroom/sections/[sectionId]/page.tsx +17 -0
- package/app/page.tsx +272 -0
- package/app/papyrus-theme.css +11 -0
- package/app/robots.ts +11 -0
- package/app/settings/page.tsx +14 -0
- package/app/tailwind.css +65 -0
- package/bin/papyrus-app.mjs +20 -0
- package/bin/papyrus-infra.mjs +95 -0
- package/components/ai-elements/conversation.tsx +168 -0
- package/components/ai-elements/message.tsx +360 -0
- package/components/ai-elements/model-selector.tsx +30 -0
- package/components/ai-elements/prompt-input.tsx +1461 -0
- package/components/ai-elements/shimmer.tsx +76 -0
- package/components/ai-elements/suggestion.tsx +54 -0
- package/components/ai-elements/tool.tsx +173 -0
- package/components/amplify-client-provider.tsx +140 -0
- package/components/archive-grid.tsx +89 -0
- package/components/archive-shell.tsx +41 -0
- package/components/article-video.tsx +156 -0
- package/components/blog-page-background.tsx +20 -0
- package/components/dev-sandbox-editor-sign-in.tsx +60 -0
- package/components/markus-ir-preview.tsx +300 -0
- package/components/news-desk-client-provider.tsx +211 -0
- package/components/news-desk-page.tsx +116 -0
- package/components/news-desk-taxonomy-client.ts +3684 -0
- package/components/newspaper.tsx +1929 -0
- package/components/newsroom-access-gate.tsx +66 -0
- package/components/newsroom-article-editor.tsx +741 -0
- package/components/newsroom-articles-view.tsx +129 -0
- package/components/newsroom-assignments-view.tsx +664 -0
- package/components/newsroom-ops-overview.tsx +226 -0
- package/components/newsroom-ops-shell.tsx +321 -0
- package/components/newsroom-references-view.tsx +598 -0
- package/components/newsroom-topics-view.tsx +425 -0
- package/components/papyrus-console-shell.tsx +1912 -0
- package/components/pictogram-figure.tsx +65 -0
- package/components/presentation-footer.tsx +146 -0
- package/components/reader-auth-control.tsx +162 -0
- package/components/reader-auth-state.ts +122 -0
- package/components/reader-settings.ts +332 -0
- package/components/reader-theme-control.tsx +85 -0
- package/components/reference-source-preview.tsx +163 -0
- package/components/settings-page.tsx +239 -0
- package/components/staging-banner.tsx +11 -0
- package/components/topic-steering-workspace.tsx +18005 -0
- package/components/ui/badge.tsx +30 -0
- package/components/ui/button-group.tsx +87 -0
- package/components/ui/button.tsx +58 -0
- package/components/ui/card.tsx +32 -0
- package/components/ui/command.tsx +194 -0
- package/components/ui/dialog.tsx +158 -0
- package/components/ui/dropdown-menu.tsx +266 -0
- package/components/ui/hover-card.tsx +51 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/scroll-area.tsx +16 -0
- package/components/ui/select.tsx +199 -0
- package/components/ui/separator.tsx +23 -0
- package/components/ui/sheet.tsx +92 -0
- package/components/ui/spinner.tsx +10 -0
- package/components/ui/tabs.tsx +113 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/components/use-resolved-papyrus-theme.ts +47 -0
- package/infra/amplify-app-shell.js +234 -0
- package/infra/amplify-app-shell.ts +284 -0
- package/infra/build-specs.js +85 -0
- package/infra/build-specs.ts +131 -0
- package/infra/github-oidc-provider.js +24 -0
- package/infra/github-oidc-provider.ts +30 -0
- package/infra/index.js +15 -0
- package/infra/site-config.js +186 -0
- package/infra/site-config.ts +235 -0
- package/lib/amplify-outputs-guard.ts +24 -0
- package/lib/amplify-outputs-path.ts +16 -0
- package/lib/amplify-server-runtime.ts +63 -0
- package/lib/archive-data.ts +30 -0
- package/lib/archive-types.ts +15 -0
- package/lib/articles.ts +320 -0
- package/lib/cached-content-repository.ts +90 -0
- package/lib/category-dashboard.ts +37 -0
- package/lib/category-repository.ts +1907 -0
- package/lib/console-chat-client.ts +1316 -0
- package/lib/console-web-context.ts +118 -0
- package/lib/content-actions-client.ts +98 -0
- package/lib/content-actions-demo.ts +190 -0
- package/lib/content-load-timing.ts +23 -0
- package/lib/content-repository.ts +43 -0
- package/lib/content-source-context.ts +26 -0
- package/lib/content-types.ts +104 -0
- package/lib/define-site.js +3 -0
- package/lib/define-site.ts +21 -0
- package/lib/demo-amplify-outputs.ts +15 -0
- package/lib/dev-sandbox-editor-auth.ts +18 -0
- package/lib/doctrine.ts +128 -0
- package/lib/edition-routes.ts +155 -0
- package/lib/edition-sections.ts +111 -0
- package/lib/empty-edition-layout-plan.ts +109 -0
- package/lib/empty-theme.css +1 -0
- package/lib/excerpts.ts +8 -0
- package/lib/graphql-content-repository.ts +1140 -0
- package/lib/image-url.ts +10 -0
- package/lib/layout-plan.ts +981 -0
- package/lib/layout-scenarios.ts +564 -0
- package/lib/markus-body.ts +168 -0
- package/lib/markus-ir.ts +235 -0
- package/lib/markus-projection.ts +243 -0
- package/lib/markus-to-article.ts +93 -0
- package/lib/news-desk-session.ts +160 -0
- package/lib/newspaper-layout.ts +3459 -0
- package/lib/newsroom-articles.ts +325 -0
- package/lib/newsroom-assignments.ts +173 -0
- package/lib/newsroom-base-path.ts +48 -0
- package/lib/newsroom-card-layout.ts +102 -0
- package/lib/newsroom-category-drilldown.ts +133 -0
- package/lib/newsroom-demo-articles.ts +90 -0
- package/lib/newsroom-demo-dashboard.ts +267 -0
- package/lib/newsroom-demo-profile.ts +46 -0
- package/lib/newsroom-forum-routes.ts +153 -0
- package/lib/newsroom-index-filters.ts +202 -0
- package/lib/newsroom-knowledge-query-request.ts +51 -0
- package/lib/newsroom-list-selection.ts +42 -0
- package/lib/newsroom-media-backend.ts +142 -0
- package/lib/newsroom-media.ts +182 -0
- package/lib/newsroom-nav.ts +44 -0
- package/lib/newsroom-references.ts +116 -0
- package/lib/newsroom-sections.ts +52 -0
- package/lib/newsroom-topics.ts +118 -0
- package/lib/papyrus-object-kinds.ts +24 -0
- package/lib/papyrus-web-locations.ts +480 -0
- package/lib/pilobol-sample.ts +167 -0
- package/lib/presentation-footer.ts +92 -0
- package/lib/pretext-layout.ts +179 -0
- package/lib/public-placeholder-config.ts +102 -0
- package/lib/publication-items.ts +181 -0
- package/lib/reader-route-config.ts +1 -0
- package/lib/reader-static-params.ts +56 -0
- package/lib/reader-storage-url.ts +63 -0
- package/lib/reference-display.ts +74 -0
- package/lib/reference-policy.ts +97 -0
- package/lib/reference-source-preview.ts +175 -0
- package/lib/renderer-config.ts +50 -0
- package/lib/renderer.ts +87 -0
- package/lib/reporting-story-budget.ts +638 -0
- package/lib/semantic-graph.ts +554 -0
- package/lib/site-brand-tenant.ts +22 -0
- package/lib/site-brand.ts +194 -0
- package/lib/site-env.ts +52 -0
- package/lib/site-renderer.ts +17 -0
- package/lib/site-stack.ts +195 -0
- package/lib/staging-gate.ts +37 -0
- package/lib/staging-preview-object.ts +98 -0
- package/lib/themed-image.ts +30 -0
- package/lib/utils.ts +6 -0
- package/lib/video-mode.ts +42 -0
- package/lib/video-script.ts +63 -0
- package/middleware.ts +103 -0
- package/package.json +318 -1
- package/renderers/markus/index.ts +25 -0
- package/renderers/markus/stubs.tsx +33 -0
- package/renderers/pretext/article-page.tsx +169 -0
- package/renderers/pretext/index.ts +30 -0
- package/renderers/pretext/layouts.ts +16 -0
- package/renderers/pretext/presentation-shell.tsx +470 -0
- package/routes.manifest.json +208 -0
- package/scripts/fixtures/pilobol-sample.ast.json +234 -0
- 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/backend.js +1 -0
- package/src/backend.ts +3 -0
- package/src/sync.mjs +87 -0
- package/src/with-papyrus.d.mts +2 -0
- package/src/with-papyrus.mjs +60 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the stored Markus body IR (`bodyIr`, the envelope written by
|
|
3
|
+
* `derive_body` in src/papyrus_content/markus_renderer/derive.py) and projects
|
|
4
|
+
* it into the flat structures the Pretext solver consumes. The solver input
|
|
5
|
+
* (`Article.body: string[]`) is unchanged; only its source changed.
|
|
6
|
+
*
|
|
7
|
+
* There is no fallback: an envelope with an unknown schema version, or a
|
|
8
|
+
* placeholder token that the envelope cannot resolve, throws `BodyIrError`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { MarkusSchemaVersionError, parseMarkusDocument } from "./markus-ir";
|
|
12
|
+
import { projectMarkusDocumentToPretext } from "./markus-projection";
|
|
13
|
+
|
|
14
|
+
export class BodyIrError extends Error {}
|
|
15
|
+
|
|
16
|
+
export type BodyProjectionImage = {
|
|
17
|
+
src: string;
|
|
18
|
+
alt: string;
|
|
19
|
+
caption?: string;
|
|
20
|
+
credit?: string;
|
|
21
|
+
layout?: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type BodyProjection = {
|
|
25
|
+
body: string[];
|
|
26
|
+
pullQuotes: string[];
|
|
27
|
+
imageSrcs: BodyProjectionImage[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const SUPPORTED_ENVELOPE_SCHEMA_VERSION = 1;
|
|
31
|
+
const SUPPORTED_MARKUS_IR_SCHEMA_VERSION = 1;
|
|
32
|
+
const TOKEN_PATTERN = /PAPYRUSMARKUP\d{5}END/g;
|
|
33
|
+
const WHOLE_TOKEN_PATTERN = /^PAPYRUSMARKUP\d{5}END$/;
|
|
34
|
+
|
|
35
|
+
type PapyrusSidecar = {
|
|
36
|
+
images: Record<string, Record<string, unknown>>;
|
|
37
|
+
citations: Record<string, string[]>;
|
|
38
|
+
citationLists: Record<string, unknown>;
|
|
39
|
+
bibliography: string[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
43
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseEnvelope(raw: unknown): Record<string, unknown> {
|
|
47
|
+
let value = raw;
|
|
48
|
+
if (typeof value === "string") {
|
|
49
|
+
try {
|
|
50
|
+
value = JSON.parse(value);
|
|
51
|
+
} catch {
|
|
52
|
+
throw new BodyIrError("bodyIr is not valid JSON");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (!isRecord(value)) throw new BodyIrError("bodyIr must be a JSON object");
|
|
56
|
+
return value;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readSidecar(envelope: Record<string, unknown>): PapyrusSidecar {
|
|
60
|
+
const sidecar = envelope.papyrus;
|
|
61
|
+
if (!isRecord(sidecar)) throw new BodyIrError("bodyIr.papyrus is missing");
|
|
62
|
+
const { images, citations, citationLists, bibliography } = sidecar;
|
|
63
|
+
if (!isRecord(images) || !isRecord(citations) || !isRecord(citationLists) || !Array.isArray(bibliography)) {
|
|
64
|
+
throw new BodyIrError("bodyIr.papyrus must contain images, citations, citationLists and bibliography");
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
images: images as PapyrusSidecar["images"],
|
|
68
|
+
citations: citations as PapyrusSidecar["citations"],
|
|
69
|
+
citationLists: citationLists,
|
|
70
|
+
bibliography: bibliography.map(String),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function numberCitationKeysByFirstAppearance(document: unknown, sidecar: PapyrusSidecar): Map<string, number> {
|
|
75
|
+
const numbers = new Map<string, number>();
|
|
76
|
+
for (const token of JSON.stringify(document).match(TOKEN_PATTERN) ?? []) {
|
|
77
|
+
for (const key of sidecar.citations[token] ?? []) {
|
|
78
|
+
if (!numbers.has(key)) numbers.set(key, numbers.size + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return numbers;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveCitationTokens(text: string, sidecar: PapyrusSidecar, numbers: Map<string, number>): string {
|
|
85
|
+
return text.replace(TOKEN_PATTERN, (token) => {
|
|
86
|
+
const keys = sidecar.citations[token];
|
|
87
|
+
if (!keys) return token;
|
|
88
|
+
return keys.map((key) => `[${numbers.get(key)}]`).join("");
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function imageFromSidecar(token: string, sidecar: PapyrusSidecar): BodyProjectionImage {
|
|
93
|
+
const entry = sidecar.images[token];
|
|
94
|
+
if (typeof entry?.src !== "string" || !entry.src) {
|
|
95
|
+
throw new BodyIrError(`bodyIr image ${token} has no src`);
|
|
96
|
+
}
|
|
97
|
+
const image: BodyProjectionImage = { src: entry.src, alt: typeof entry.alt === "string" ? entry.alt : "" };
|
|
98
|
+
if (typeof entry.caption === "string" && entry.caption) image.caption = entry.caption;
|
|
99
|
+
if (typeof entry.credit === "string" && entry.credit) image.credit = entry.credit;
|
|
100
|
+
if (typeof entry.layout === "string" && entry.layout) image.layout = entry.layout;
|
|
101
|
+
return image;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function projectBodyIr(raw: unknown): BodyProjection {
|
|
105
|
+
const envelope = parseEnvelope(raw);
|
|
106
|
+
if (envelope.schemaVersion !== SUPPORTED_ENVELOPE_SCHEMA_VERSION) {
|
|
107
|
+
throw new BodyIrError(
|
|
108
|
+
`bodyIr schemaVersion ${String(envelope.schemaVersion)} is not supported (expected ${SUPPORTED_ENVELOPE_SCHEMA_VERSION})`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const markus = envelope.markus;
|
|
112
|
+
if (!isRecord(markus) || markus.irSchemaVersion !== SUPPORTED_MARKUS_IR_SCHEMA_VERSION) {
|
|
113
|
+
throw new BodyIrError(
|
|
114
|
+
`bodyIr markus.irSchemaVersion ${String(isRecord(markus) ? markus.irSchemaVersion : undefined)} is not supported (expected ${SUPPORTED_MARKUS_IR_SCHEMA_VERSION})`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const sidecar = readSidecar(envelope);
|
|
118
|
+
|
|
119
|
+
let projected;
|
|
120
|
+
try {
|
|
121
|
+
projected = projectMarkusDocumentToPretext(parseMarkusDocument(envelope.document));
|
|
122
|
+
} catch (error) {
|
|
123
|
+
if (error instanceof MarkusSchemaVersionError) throw new BodyIrError(error.message);
|
|
124
|
+
throw error;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const numbers = numberCitationKeysByFirstAppearance(envelope.document, sidecar);
|
|
128
|
+
const body: string[] = [];
|
|
129
|
+
const imageSrcs: BodyProjectionImage[] = [];
|
|
130
|
+
for (const paragraph of projected.body) {
|
|
131
|
+
if (WHOLE_TOKEN_PATTERN.test(paragraph) && paragraph in sidecar.images) {
|
|
132
|
+
imageSrcs.push(imageFromSidecar(paragraph, sidecar));
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (WHOLE_TOKEN_PATTERN.test(paragraph) && paragraph in sidecar.citationLists) {
|
|
136
|
+
sidecar.bibliography.forEach((entry, index) => body.push(`${index + 1}. ${entry}`));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
body.push(resolveCitationTokens(paragraph, sidecar, numbers));
|
|
140
|
+
}
|
|
141
|
+
const pullQuotes = projected.pullQuotes.map((quote) => resolveCitationTokens(quote, sidecar, numbers));
|
|
142
|
+
|
|
143
|
+
if ([...body, ...pullQuotes].some((text) => /PAPYRUSMARKUP\d{5}END/.test(text))) {
|
|
144
|
+
throw new BodyIrError("bodyIr contains a placeholder token with no entry in bodyIr.papyrus");
|
|
145
|
+
}
|
|
146
|
+
return { body, pullQuotes, imageSrcs };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export type BodyImageMatch =
|
|
150
|
+
| { kind: "media"; mediaIndex: number }
|
|
151
|
+
| { kind: "external"; image: BodyProjectionImage };
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Matches each image the body references to a stored media row by the
|
|
155
|
+
* `srcPath` the row was created from. Absolute http(s) sources need no media
|
|
156
|
+
* row. Any other source without a row is an error, never skipped.
|
|
157
|
+
*/
|
|
158
|
+
export function matchBodyImages(
|
|
159
|
+
imageSrcs: BodyProjectionImage[],
|
|
160
|
+
mediaSrcPaths: Array<string | null>,
|
|
161
|
+
): BodyImageMatch[] {
|
|
162
|
+
return imageSrcs.map((image) => {
|
|
163
|
+
const mediaIndex = mediaSrcPaths.indexOf(image.src);
|
|
164
|
+
if (mediaIndex >= 0) return { kind: "media", mediaIndex };
|
|
165
|
+
if (/^https?:\/\//i.test(image.src)) return { kind: "external", image };
|
|
166
|
+
throw new BodyIrError(`bodyIr references image "${image.src}" but no media asset has that srcPath`);
|
|
167
|
+
});
|
|
168
|
+
}
|
package/lib/markus-ir.ts
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed TypeScript mirror of the Markus document IR (`markus ast`, Markus
|
|
3
|
+
* repo branch `feature/document-ir`, `src/markusmd/ast.py` +
|
|
4
|
+
* `src/markusmd/blocks.py`).
|
|
5
|
+
*
|
|
6
|
+
* This is NOT invented from the Markus language docs -- it is typed
|
|
7
|
+
* directly from a real `markus ast` invocation (see
|
|
8
|
+
* `scripts/fixtures/markus-sample.ast.json`, captured from
|
|
9
|
+
* `scripts/fixtures/markus-sample.md` via `markus ast`) cross-checked
|
|
10
|
+
* against the emitting Python dataclasses' `to_dict()` methods. Papyrus
|
|
11
|
+
* does NOT re-parse Markdown; it only ingests this JSON shape.
|
|
12
|
+
*
|
|
13
|
+
* `IR_SCHEMA_VERSION` mirrors `markusmd.blocks.IR_SCHEMA_VERSION`. Bump it
|
|
14
|
+
* only in lockstep with a corresponding bump on the Markus side, and only
|
|
15
|
+
* after re-deriving the types below from a fresh `markus ast` capture --
|
|
16
|
+
* never by hand-editing the number.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** The Markus document IR schema version Papyrus is typed against. */
|
|
20
|
+
export const MARKUS_IR_SCHEMA_VERSION = 1 as const;
|
|
21
|
+
|
|
22
|
+
// --------------------------------------------------------------------------
|
|
23
|
+
// Inline nodes (markusmd/blocks.py: Text | Emphasis | Strong | Strikethrough
|
|
24
|
+
// | CodeSpan | Link | Image | SoftBreak | HardBreak | HtmlInline)
|
|
25
|
+
// --------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export type MarkusInlineText = { type: "text"; text: string };
|
|
28
|
+
|
|
29
|
+
export type MarkusInlineEmphasis = { type: "emphasis"; children: MarkusInline[] };
|
|
30
|
+
|
|
31
|
+
export type MarkusInlineStrong = { type: "strong"; children: MarkusInline[] };
|
|
32
|
+
|
|
33
|
+
export type MarkusInlineStrikethrough = { type: "strikethrough"; children: MarkusInline[] };
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* An inline code span, e.g. `` `code` ``. Distinct from the block-level
|
|
37
|
+
* `code` node (`MarkusCodeBlock`, type `"code"`) below -- do not conflate
|
|
38
|
+
* the two. `code_span` never has a `lang`; it is always plain text.
|
|
39
|
+
*/
|
|
40
|
+
export type MarkusInlineCodeSpan = { type: "code_span"; code: string };
|
|
41
|
+
|
|
42
|
+
export type MarkusInlineLink = {
|
|
43
|
+
type: "link";
|
|
44
|
+
href: string | null;
|
|
45
|
+
title: string | null;
|
|
46
|
+
children: MarkusInline[];
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type MarkusInlineImage = {
|
|
50
|
+
type: "image";
|
|
51
|
+
src: string | null;
|
|
52
|
+
alt: string;
|
|
53
|
+
title: string | null;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type MarkusInlineSoftBreak = { type: "soft_break" };
|
|
57
|
+
|
|
58
|
+
export type MarkusInlineHardBreak = { type: "hard_break" };
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Raw inline HTML. Only produced when the document was parsed with
|
|
62
|
+
* `allow_html=True` (`markus ast --allow-html`). Papyrus builds fragments
|
|
63
|
+
* with `--allow-html` OFF (see docs/pluggable-publishers security note), so
|
|
64
|
+
* this node type should not appear in IR Papyrus ingests in practice; it is
|
|
65
|
+
* still typed here because the emitter can produce it, and the projection
|
|
66
|
+
* must not silently drop it if it ever does.
|
|
67
|
+
*/
|
|
68
|
+
export type MarkusInlineHtml = { type: "html_inline"; value: string };
|
|
69
|
+
|
|
70
|
+
export type MarkusInline =
|
|
71
|
+
| MarkusInlineText
|
|
72
|
+
| MarkusInlineEmphasis
|
|
73
|
+
| MarkusInlineStrong
|
|
74
|
+
| MarkusInlineStrikethrough
|
|
75
|
+
| MarkusInlineCodeSpan
|
|
76
|
+
| MarkusInlineLink
|
|
77
|
+
| MarkusInlineImage
|
|
78
|
+
| MarkusInlineSoftBreak
|
|
79
|
+
| MarkusInlineHardBreak
|
|
80
|
+
| MarkusInlineHtml;
|
|
81
|
+
|
|
82
|
+
// --------------------------------------------------------------------------
|
|
83
|
+
// Typed Markdown block nodes (markusmd/blocks.py: Heading | Paragraph |
|
|
84
|
+
// List | Blockquote | CodeBlock | ThematicBreak | HtmlBlock | Table).
|
|
85
|
+
// These sit as ordered PEERS of directive nodes in one children[] array --
|
|
86
|
+
// there is no opaque raw-markdown node in the IR.
|
|
87
|
+
// --------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
export type MarkusHeading = {
|
|
90
|
+
type: "heading";
|
|
91
|
+
level: number;
|
|
92
|
+
inline: MarkusInline[];
|
|
93
|
+
line: number | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export type MarkusParagraph = {
|
|
97
|
+
type: "paragraph";
|
|
98
|
+
inline: MarkusInline[];
|
|
99
|
+
line: number | null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type MarkusListItem = {
|
|
103
|
+
type: "list_item";
|
|
104
|
+
checked: boolean | null;
|
|
105
|
+
children: MarkusBlock[];
|
|
106
|
+
line: number | null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export type MarkusList = {
|
|
110
|
+
type: "list";
|
|
111
|
+
ordered: boolean;
|
|
112
|
+
start: number | null;
|
|
113
|
+
items: MarkusListItem[];
|
|
114
|
+
line: number | null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
export type MarkusBlockquote = {
|
|
118
|
+
type: "blockquote";
|
|
119
|
+
children: MarkusBlock[];
|
|
120
|
+
line: number | null;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A fenced or indented code BLOCK. Distinct from the inline `code_span`
|
|
125
|
+
* node above -- do not conflate the two.
|
|
126
|
+
*/
|
|
127
|
+
export type MarkusCodeBlock = {
|
|
128
|
+
type: "code";
|
|
129
|
+
lang: string | null;
|
|
130
|
+
value: string;
|
|
131
|
+
line: number | null;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type MarkusThematicBreak = { type: "thematic_break"; line: number | null };
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* A raw HTML block. Only produced when `allow_html=True`; see
|
|
138
|
+
* `MarkusInlineHtml` above for why Papyrus's build should never emit this
|
|
139
|
+
* in the IR it ingests, and why it is still typed and handled anyway.
|
|
140
|
+
*/
|
|
141
|
+
export type MarkusHtmlBlock = { type: "html"; value: string; line: number | null };
|
|
142
|
+
|
|
143
|
+
export type MarkusTable = {
|
|
144
|
+
type: "table";
|
|
145
|
+
align: Array<string | null>;
|
|
146
|
+
header: MarkusInline[][];
|
|
147
|
+
rows: MarkusInline[][][];
|
|
148
|
+
line: number | null;
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
export type MarkusBlock =
|
|
152
|
+
| MarkusHeading
|
|
153
|
+
| MarkusParagraph
|
|
154
|
+
| MarkusList
|
|
155
|
+
| MarkusBlockquote
|
|
156
|
+
| MarkusCodeBlock
|
|
157
|
+
| MarkusThematicBreak
|
|
158
|
+
| MarkusHtmlBlock
|
|
159
|
+
| MarkusTable;
|
|
160
|
+
|
|
161
|
+
// --------------------------------------------------------------------------
|
|
162
|
+
// Directives (markusmd/ast.py: Directive). Container directives have
|
|
163
|
+
// `leaf: false` and nest further nodes; leaf directives (`metric`, `video`)
|
|
164
|
+
// have `leaf: true` and always `children: []`. `attributes` is whatever the
|
|
165
|
+
// directive's validated attribute schema produced -- Papyrus treats it as
|
|
166
|
+
// an open record rather than typing every directive's attribute set, since
|
|
167
|
+
// the vocabulary (and its attributes) are owned and validated by Markus,
|
|
168
|
+
// not re-specified here.
|
|
169
|
+
// --------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
export type MarkusDirectiveAttributes = Record<string, string | number | boolean | null>;
|
|
172
|
+
|
|
173
|
+
export type MarkusDirective = {
|
|
174
|
+
type: "directive";
|
|
175
|
+
name: string;
|
|
176
|
+
attributes: MarkusDirectiveAttributes;
|
|
177
|
+
leaf: boolean;
|
|
178
|
+
line: number;
|
|
179
|
+
children: MarkusNode[];
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
/** A document child: a typed Markdown block or a directive, in document order. */
|
|
183
|
+
export type MarkusNode = MarkusBlock | MarkusDirective;
|
|
184
|
+
|
|
185
|
+
// --------------------------------------------------------------------------
|
|
186
|
+
// Document root (markusmd/ast.py: Document.to_dict()).
|
|
187
|
+
// --------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
export type MarkusFrontMatter = Record<string, unknown>;
|
|
190
|
+
|
|
191
|
+
export type MarkusDocument = {
|
|
192
|
+
type: "document";
|
|
193
|
+
schema_version: number;
|
|
194
|
+
front_matter: MarkusFrontMatter;
|
|
195
|
+
children: MarkusNode[];
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Thrown when ingested IR declares a `schema_version` this module was not
|
|
200
|
+
* typed against. This is the "fail loudly on mismatch" boundary check
|
|
201
|
+
* required before any projection runs -- silently proceeding against an
|
|
202
|
+
* unknown shape would risk mis-typed access on fields that changed shape.
|
|
203
|
+
*/
|
|
204
|
+
export class MarkusSchemaVersionError extends Error {
|
|
205
|
+
constructor(public readonly received: unknown) {
|
|
206
|
+
super(
|
|
207
|
+
`Markus document IR schema_version mismatch: this build understands ` +
|
|
208
|
+
`schema_version ${MARKUS_IR_SCHEMA_VERSION}, but received ` +
|
|
209
|
+
`${JSON.stringify(received)}. Re-derive lib/markus-ir.ts from a fresh ` +
|
|
210
|
+
`\`markus ast\` capture before ingesting this document.`,
|
|
211
|
+
);
|
|
212
|
+
this.name = "MarkusSchemaVersionError";
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parse + validate a raw `markus ast` JSON payload into a typed
|
|
218
|
+
* `MarkusDocument`, checking `schema_version` at the boundary. This is the
|
|
219
|
+
* only place Papyrus should accept untyped Markus IR JSON -- everything
|
|
220
|
+
* downstream (the Pretext projection, a future Markus build step) should
|
|
221
|
+
* consume the typed `MarkusDocument` this returns, never raw `unknown`.
|
|
222
|
+
*/
|
|
223
|
+
export function parseMarkusDocument(raw: unknown): MarkusDocument {
|
|
224
|
+
if (typeof raw !== "object" || raw === null) {
|
|
225
|
+
throw new Error("Markus document IR must be a JSON object");
|
|
226
|
+
}
|
|
227
|
+
const candidate = raw as Record<string, unknown>;
|
|
228
|
+
if (candidate.type !== "document") {
|
|
229
|
+
throw new Error(`Expected Markus IR root node of type "document", got ${JSON.stringify(candidate.type)}`);
|
|
230
|
+
}
|
|
231
|
+
if (candidate.schema_version !== MARKUS_IR_SCHEMA_VERSION) {
|
|
232
|
+
throw new MarkusSchemaVersionError(candidate.schema_version);
|
|
233
|
+
}
|
|
234
|
+
return candidate as MarkusDocument;
|
|
235
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Pretext projection: turns a typed Markus document IR
|
|
3
|
+
* (`lib/markus-ir.ts`) into the flat structures the Pretext client solver
|
|
4
|
+
* consumes today -- `Article.body: string[]`, `Article.pullQuotes: string[]`,
|
|
5
|
+
* and figure-derived `ArticleImageAsset[]`.
|
|
6
|
+
*
|
|
7
|
+
* THIS PROJECTION IS LOSSY BY DESIGN. Markus's directive vocabulary
|
|
8
|
+
* (card-grid, two-up, callout, tabs, timeline, ...) is a strict superset of
|
|
9
|
+
* what Pretext can lay out -- Pretext cannot render a card grid. The
|
|
10
|
+
* non-negotiable degradation rule (decided on PPY-92f846 / PPY-a10206):
|
|
11
|
+
*
|
|
12
|
+
* Any directive Pretext cannot express recurses into its children so its
|
|
13
|
+
* content is preserved INLINE in `body`. Nothing is ever silently
|
|
14
|
+
* dropped.
|
|
15
|
+
*
|
|
16
|
+
* The two directives Pretext *can* express get pulled into their own
|
|
17
|
+
* side-channels instead of `body`:
|
|
18
|
+
* - `pull-quote` -> `pullQuotes[]`
|
|
19
|
+
* - `figure` -> `images[]` (as `ArticleImageAsset`)
|
|
20
|
+
*
|
|
21
|
+
* This mirrors the verified prototype recorded on PPY-a10206: walk
|
|
22
|
+
* `children` in order; paragraph/heading push their flattened text onto
|
|
23
|
+
* `body`; `pull-quote` pushes onto `pullQuotes`; any other directive
|
|
24
|
+
* recurses into its children. This module generalizes that same rule to
|
|
25
|
+
* the rest of the typed block vocabulary (list/blockquote/code/table/
|
|
26
|
+
* thematic_break/html) that the prototype's sample article didn't happen
|
|
27
|
+
* to exercise, and to leaf directives with no children (`metric`, `video`)
|
|
28
|
+
* by degrading to their textual attributes rather than dropping them.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ArticleImageAsset } from "./articles";
|
|
32
|
+
import {
|
|
33
|
+
parseMarkusDocument,
|
|
34
|
+
type MarkusBlock,
|
|
35
|
+
type MarkusDirective,
|
|
36
|
+
type MarkusDocument,
|
|
37
|
+
type MarkusInline,
|
|
38
|
+
type MarkusNode,
|
|
39
|
+
} from "./markus-ir";
|
|
40
|
+
|
|
41
|
+
export type PretextProjection = {
|
|
42
|
+
body: string[];
|
|
43
|
+
pullQuotes: string[];
|
|
44
|
+
images: ArticleImageAsset[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Flatten an inline node tree to plain text. No markup survives -- Pretext's
|
|
48
|
+
* `body` is plain-text paragraphs, so emphasis/strong/links/code spans all
|
|
49
|
+
* degrade to their text content. Images are dropped here (they are not
|
|
50
|
+
* textual); a top-level `image` inline node inside a paragraph is rare in
|
|
51
|
+
* practice and, if it occurs, its alt text is preserved instead of the
|
|
52
|
+
* image itself, so no content vanishes silently. */
|
|
53
|
+
function inlineText(nodes: MarkusInline[]): string {
|
|
54
|
+
return nodes
|
|
55
|
+
.map((node): string => {
|
|
56
|
+
switch (node.type) {
|
|
57
|
+
case "text":
|
|
58
|
+
return node.text;
|
|
59
|
+
case "code_span":
|
|
60
|
+
return node.code;
|
|
61
|
+
case "emphasis":
|
|
62
|
+
case "strong":
|
|
63
|
+
case "strikethrough":
|
|
64
|
+
return inlineText(node.children);
|
|
65
|
+
case "link":
|
|
66
|
+
return inlineText(node.children);
|
|
67
|
+
case "image":
|
|
68
|
+
return node.alt ?? "";
|
|
69
|
+
case "soft_break":
|
|
70
|
+
return " ";
|
|
71
|
+
case "hard_break":
|
|
72
|
+
return "\n";
|
|
73
|
+
case "html_inline":
|
|
74
|
+
// --allow-html is off for real Papyrus builds (see module doc);
|
|
75
|
+
// preserve the raw text rather than silently dropping it in the
|
|
76
|
+
// defensive case this ever appears.
|
|
77
|
+
return node.value;
|
|
78
|
+
default:
|
|
79
|
+
return "";
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.join("")
|
|
83
|
+
.trim();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Flatten a block node's own text content (not directives -- callers walk
|
|
87
|
+
* directives separately so their degradation/side-channel rules apply). */
|
|
88
|
+
function blockText(block: MarkusBlock): string[] {
|
|
89
|
+
switch (block.type) {
|
|
90
|
+
case "heading":
|
|
91
|
+
case "paragraph":
|
|
92
|
+
return [inlineText(block.inline)];
|
|
93
|
+
case "list":
|
|
94
|
+
return block.items.map((item) => flattenBlocksToText(item.children).join(" "));
|
|
95
|
+
case "blockquote":
|
|
96
|
+
return flattenBlocksToText(block.children);
|
|
97
|
+
case "code":
|
|
98
|
+
// A code block has no inline richness; preserve its literal text
|
|
99
|
+
// wholesale rather than dropping it because Pretext has no code
|
|
100
|
+
// rendering of its own.
|
|
101
|
+
return block.value.trim().length > 0 ? [block.value] : [];
|
|
102
|
+
case "thematic_break":
|
|
103
|
+
// No textual content exists to preserve; there is nothing to drop.
|
|
104
|
+
return [];
|
|
105
|
+
case "html":
|
|
106
|
+
// See `inlineText`'s html_inline case: only reachable if IR was ever
|
|
107
|
+
// produced with --allow-html, which Papyrus's build does not do.
|
|
108
|
+
return block.value.trim().length > 0 ? [block.value] : [];
|
|
109
|
+
case "table": {
|
|
110
|
+
const rowText = (row: MarkusInline[][]) => row.map((cell) => inlineText(cell)).join(" | ");
|
|
111
|
+
const lines = [rowText(block.header), ...block.rows.map(rowText)];
|
|
112
|
+
return lines.filter((line) => line.trim().length > 0);
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function isDirective(node: MarkusNode): node is MarkusDirective {
|
|
120
|
+
return node.type === "directive";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Degrade a leaf directive (no children by definition) to its textual
|
|
124
|
+
* attributes, so `metric`/`video` don't vanish just because they have
|
|
125
|
+
* nothing to recurse into. */
|
|
126
|
+
function leafDirectiveText(directive: MarkusDirective): string[] {
|
|
127
|
+
const attrs = directive.attributes;
|
|
128
|
+
if (directive.name === "metric") {
|
|
129
|
+
const label = attrs.label != null ? String(attrs.label) : null;
|
|
130
|
+
const value = attrs.value != null ? String(attrs.value) : null;
|
|
131
|
+
const unit = attrs.unit != null ? String(attrs.unit) : "";
|
|
132
|
+
const text = [label, value != null ? `${value}${unit}` : null].filter(Boolean).join(": ");
|
|
133
|
+
return text.length > 0 ? [text] : [];
|
|
134
|
+
}
|
|
135
|
+
if (directive.name === "video") {
|
|
136
|
+
const title = attrs.title != null ? String(attrs.title) : null;
|
|
137
|
+
return title ? [title] : [];
|
|
138
|
+
}
|
|
139
|
+
// Unknown future leaf directive: fall back to whatever string-ish
|
|
140
|
+
// attributes it has rather than dropping it silently.
|
|
141
|
+
const text = Object.values(attrs)
|
|
142
|
+
.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
143
|
+
.join(" ");
|
|
144
|
+
return text ? [text] : [];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Extract an ArticleImageAsset from a `figure` directive's attributes. */
|
|
148
|
+
function figureToImageAsset(directive: MarkusDirective, index: number): ArticleImageAsset {
|
|
149
|
+
const attrs = directive.attributes;
|
|
150
|
+
const src = attrs.src != null ? String(attrs.src) : "";
|
|
151
|
+
const alt = attrs.alt != null ? String(attrs.alt) : "";
|
|
152
|
+
const caption = attrs.caption != null ? String(attrs.caption) : undefined;
|
|
153
|
+
const credit = attrs.credit != null ? String(attrs.credit) : "";
|
|
154
|
+
return {
|
|
155
|
+
id: `figure-${index}`,
|
|
156
|
+
type: "image",
|
|
157
|
+
src,
|
|
158
|
+
alt,
|
|
159
|
+
caption,
|
|
160
|
+
credit,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function flattenBlocksToText(nodes: MarkusNode[]): string[] {
|
|
165
|
+
const out: string[] = [];
|
|
166
|
+
for (const node of nodes) {
|
|
167
|
+
if (isDirective(node)) {
|
|
168
|
+
out.push(...directiveDegradedText(node));
|
|
169
|
+
} else {
|
|
170
|
+
out.push(...blockText(node));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return out.filter((line) => line.trim().length > 0);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The degradation rule for a directive Pretext cannot render as itself:
|
|
177
|
+
* recurse into its children (or, for a leaf directive with no children,
|
|
178
|
+
* degrade to its attributes) so nothing is silently dropped. */
|
|
179
|
+
function directiveDegradedText(directive: MarkusDirective): string[] {
|
|
180
|
+
if (directive.leaf || directive.children.length === 0) {
|
|
181
|
+
return leafDirectiveText(directive);
|
|
182
|
+
}
|
|
183
|
+
return flattenBlocksToText(directive.children);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Project a typed Markus document IR into the flat shape Pretext consumes.
|
|
188
|
+
* Walks `document.children` in order:
|
|
189
|
+
* - `paragraph` / `heading` -> pushed onto `body`
|
|
190
|
+
* - `list` / `blockquote` / `code` / `table` / `thematic_break` / `html`
|
|
191
|
+
* -> degrade-preserved onto `body` (see `blockText`)
|
|
192
|
+
* - directive `pull-quote` -> its flattened text goes to `pullQuotes`,
|
|
193
|
+
* NOT `body`
|
|
194
|
+
* - directive `figure` -> becomes an `ArticleImageAsset` in `images`,
|
|
195
|
+
* NOT `body`
|
|
196
|
+
* - any other directive (`card-grid`, `card`, `two-up`, `column`,
|
|
197
|
+
* `callout`, `aside`, `details`, `tabs`, `tab`, `step-list`, `step`,
|
|
198
|
+
* `timeline`, `timeline-event`, and any future addition to the
|
|
199
|
+
* vocabulary) -> recurses into its children per the degradation rule
|
|
200
|
+
*/
|
|
201
|
+
export function projectMarkusDocumentToPretext(document: MarkusDocument): PretextProjection {
|
|
202
|
+
const body: string[] = [];
|
|
203
|
+
const pullQuotes: string[] = [];
|
|
204
|
+
const images: ArticleImageAsset[] = [];
|
|
205
|
+
let figureIndex = 0;
|
|
206
|
+
|
|
207
|
+
for (const node of document.children) {
|
|
208
|
+
if (isDirective(node)) {
|
|
209
|
+
if (node.name === "pull-quote") {
|
|
210
|
+
const text = flattenBlocksToText(node.children).join(" ");
|
|
211
|
+
if (text.trim().length > 0) pullQuotes.push(text);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (node.name === "figure") {
|
|
215
|
+
images.push(figureToImageAsset(node, figureIndex));
|
|
216
|
+
figureIndex += 1;
|
|
217
|
+
// A figure directive's own children (if any future Markus version
|
|
218
|
+
// ever nests content under it) are still degraded into body so
|
|
219
|
+
// nothing is dropped, matching the non-negotiable rule.
|
|
220
|
+
if (node.children.length > 0) body.push(...flattenBlocksToText(node.children));
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
body.push(...directiveDegradedText(node));
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
body.push(...blockText(node));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return {
|
|
230
|
+
body: body.filter((line) => line.trim().length > 0),
|
|
231
|
+
pullQuotes,
|
|
232
|
+
images,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Convenience entry point: parse + validate raw `markus ast` JSON (failing
|
|
238
|
+
* loudly on schema mismatch, per `parseMarkusDocument`) and project it in
|
|
239
|
+
* one call.
|
|
240
|
+
*/
|
|
241
|
+
export function projectMarkusJsonToPretext(raw: unknown): PretextProjection {
|
|
242
|
+
return projectMarkusDocumentToPretext(parseMarkusDocument(raw));
|
|
243
|
+
}
|