@inneropen/marvin-astro 1.1.0 → 1.2.0
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/README.md +175 -9
- package/dist/embeds-BTNryQA6.d.ts +79 -0
- package/dist/index.d.ts +42 -3
- package/dist/index.js +143 -9
- package/dist/index.js.map +1 -1
- package/dist/renderers/index.d.ts +119 -0
- package/dist/renderers/index.js +251 -0
- package/dist/renderers/index.js.map +1 -0
- package/dist/types.d.ts +8 -1
- package/package.json +16 -6
- package/src/chrome.ts +242 -0
- package/src/client.ts +119 -0
- package/src/components/Embed.astro +119 -0
- package/src/components/EmbedLoader.astro +24 -0
- package/src/components/EntryRenderer.astro +40 -0
- package/src/components/FormRenderer.astro +188 -0
- package/src/components/LinkCard.astro +42 -0
- package/src/components/embed-frame.ts +168 -0
- package/src/components/embeds.css +137 -0
- package/src/components/index.ts +29 -0
- package/src/components/integration.ts +62 -0
- package/src/components/markdown.ts +4 -0
- package/src/components/registry.ts +34 -0
- package/src/components/renderers/ArticleRenderer.astro +56 -0
- package/src/components/renderers/FaqRenderer.astro +26 -0
- package/src/components/renderers/NavigationRenderer.astro +27 -0
- package/src/components/renderers/PageRenderer.astro +51 -0
- package/src/config.ts +149 -0
- package/src/embeds.ts +137 -0
- package/src/env.d.ts +5 -0
- package/src/fetch.ts +198 -0
- package/src/fields.ts +330 -0
- package/src/format.ts +63 -0
- package/src/index.ts +169 -0
- package/src/markdown.ts +84 -0
- package/src/normalize.ts +284 -0
- package/src/renderers/debug.ts +6 -0
- package/src/renderers/index.ts +48 -0
- package/src/renderers/registry.ts +133 -0
- package/src/renderers/resolve.ts +134 -0
- package/src/renderers/types.ts +71 -0
- package/src/renderers/validation-types.ts +23 -0
- package/src/renderers/validation.ts +55 -0
- package/src/repository.ts +190 -0
- package/src/sections.ts +39 -0
- package/src/site.ts +222 -0
- package/src/types.ts +188 -0
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ var ENV_KEYS = {
|
|
|
9
9
|
debug: "MARVIN_DEBUG"
|
|
10
10
|
};
|
|
11
11
|
var DEFAULT_DEV_RETRY_MS = 3e4;
|
|
12
|
+
var DEFAULT_HYDRATE_CONCURRENCY = 6;
|
|
12
13
|
function metaEnv() {
|
|
13
14
|
try {
|
|
14
15
|
return import.meta.env;
|
|
@@ -47,6 +48,7 @@ function resolveConfig(options = {}) {
|
|
|
47
48
|
workspaceSlug,
|
|
48
49
|
debug,
|
|
49
50
|
retryAfterMs: options.retryAfterMs ?? (isProduction(env) ? Number.POSITIVE_INFINITY : DEFAULT_DEV_RETRY_MS),
|
|
51
|
+
hydrateConcurrency: Math.max(1, Math.floor(options.hydrateConcurrency ?? DEFAULT_HYDRATE_CONCURRENCY)),
|
|
50
52
|
logger: options.logger ?? console,
|
|
51
53
|
markdown: options.markdown,
|
|
52
54
|
now: options.now ?? (() => Date.now()),
|
|
@@ -402,6 +404,24 @@ function createChromeLoader(fetcher, siteLoader, options = {}) {
|
|
|
402
404
|
}
|
|
403
405
|
|
|
404
406
|
// src/fetch.ts
|
|
407
|
+
var HYDRATE_ATTEMPTS = 3;
|
|
408
|
+
var HYDRATE_BACKOFF_MS = 500;
|
|
409
|
+
function sleep(ms) {
|
|
410
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
411
|
+
}
|
|
412
|
+
async function mapWithConcurrency(items, limit, run) {
|
|
413
|
+
const results = new Array(items.length);
|
|
414
|
+
let next = 0;
|
|
415
|
+
async function worker() {
|
|
416
|
+
while (next < items.length) {
|
|
417
|
+
const index = next++;
|
|
418
|
+
results[index] = await run(items[index]);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
const workers = Array.from({ length: Math.min(limit, items.length) }, worker);
|
|
422
|
+
await Promise.all(workers);
|
|
423
|
+
return results;
|
|
424
|
+
}
|
|
405
425
|
function createFetcher(backend) {
|
|
406
426
|
async function guarded(label, empty, run) {
|
|
407
427
|
if (!backend.hasBackend()) return empty;
|
|
@@ -416,14 +436,36 @@ function createFetcher(backend) {
|
|
|
416
436
|
async function entry(slug) {
|
|
417
437
|
return guarded(`Entry "${slug}"`, null, () => backend.client().entry(slug));
|
|
418
438
|
}
|
|
439
|
+
async function entryForHydration(slug) {
|
|
440
|
+
if (!backend.hasBackend()) return null;
|
|
441
|
+
let failure;
|
|
442
|
+
for (let attempt = 1; attempt <= HYDRATE_ATTEMPTS; attempt++) {
|
|
443
|
+
if (attempt > 1) {
|
|
444
|
+
await sleep(HYDRATE_BACKOFF_MS * 2 ** (attempt - 2));
|
|
445
|
+
if (backend.isLatched()) backend.clearLatch();
|
|
446
|
+
}
|
|
447
|
+
try {
|
|
448
|
+
return await backend.client().entry(slug);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
failure = error;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
backend.remember(failure);
|
|
454
|
+
backend.warn(
|
|
455
|
+
`Entry "${slug}" dropped after ${HYDRATE_ATTEMPTS} attempts: ${errorMessage(failure)}`
|
|
456
|
+
);
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
419
459
|
async function hydrate(entries) {
|
|
420
|
-
const hydrated = await
|
|
421
|
-
entries
|
|
460
|
+
const hydrated = await mapWithConcurrency(
|
|
461
|
+
entries,
|
|
462
|
+
backend.config.hydrateConcurrency,
|
|
463
|
+
async (item) => {
|
|
422
464
|
if (typeof item.field === "function" || "data" in item || "dataJson" in item) {
|
|
423
465
|
return item;
|
|
424
466
|
}
|
|
425
|
-
return
|
|
426
|
-
}
|
|
467
|
+
return entryForHydration(item.slug ?? "");
|
|
468
|
+
}
|
|
427
469
|
);
|
|
428
470
|
return hydrated.filter((item) => Boolean(item));
|
|
429
471
|
}
|
|
@@ -485,13 +527,79 @@ function createFetcher(backend) {
|
|
|
485
527
|
|
|
486
528
|
// src/markdown.ts
|
|
487
529
|
import { Marked } from "marked";
|
|
530
|
+
|
|
531
|
+
// src/embeds.ts
|
|
532
|
+
var EMBED_MODES = /* @__PURE__ */ new Set(["direct", "click_to_load"]);
|
|
533
|
+
function readEmbedsField(entry) {
|
|
534
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
535
|
+
const direct = entry.embeds;
|
|
536
|
+
if (direct !== void 0) return direct;
|
|
537
|
+
const toJSON = entry.toJSON;
|
|
538
|
+
if (typeof toJSON === "function") {
|
|
539
|
+
return asRecord(toJSON.call(entry)).embeds;
|
|
540
|
+
}
|
|
541
|
+
return void 0;
|
|
542
|
+
}
|
|
543
|
+
function entryEmbeds(entry) {
|
|
544
|
+
const raw = readEmbedsField(entry);
|
|
545
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
|
|
546
|
+
const embeds = {};
|
|
547
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
548
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
549
|
+
embeds[key] = value;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return Object.keys(embeds).length > 0 ? embeds : void 0;
|
|
553
|
+
}
|
|
554
|
+
function findEmbed(embeds, url) {
|
|
555
|
+
if (!embeds || !url) return void 0;
|
|
556
|
+
const key = url.trim();
|
|
557
|
+
if (!key) return void 0;
|
|
558
|
+
return Object.prototype.hasOwnProperty.call(embeds, key) ? embeds[key] : void 0;
|
|
559
|
+
}
|
|
560
|
+
function siteEmbeds(raw) {
|
|
561
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
562
|
+
const value = asRecord(raw);
|
|
563
|
+
const mode = asString(value.mode);
|
|
564
|
+
const frameSources = Array.isArray(value.frameSources) ? value.frameSources.filter(
|
|
565
|
+
(source) => typeof source === "string" && source.trim().length > 0
|
|
566
|
+
) : [];
|
|
567
|
+
return {
|
|
568
|
+
// Unknown or missing mode → the privacy-preserving default.
|
|
569
|
+
mode: mode && EMBED_MODES.has(mode) ? mode : "click_to_load",
|
|
570
|
+
consentText: asString(value.consentText) ?? "",
|
|
571
|
+
frameSources
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/markdown.ts
|
|
576
|
+
var EMBEDS = /* @__PURE__ */ Symbol("marvin.embeds");
|
|
577
|
+
var BARE_URL = /^https?:\/\/\S+$/i;
|
|
578
|
+
function paragraphEmbedHtml(token, embeds) {
|
|
579
|
+
if (!embeds) return void 0;
|
|
580
|
+
const text = token.text.trim();
|
|
581
|
+
if (!BARE_URL.test(text)) return void 0;
|
|
582
|
+
const html = findEmbed(embeds, text)?.html;
|
|
583
|
+
return typeof html === "string" && html.trim() ? html : void 0;
|
|
584
|
+
}
|
|
488
585
|
function createMarkdownRenderer(options = {}) {
|
|
489
586
|
const marked = new Marked();
|
|
490
587
|
marked.setOptions({ gfm: true, breaks: false, ...options });
|
|
491
|
-
|
|
588
|
+
marked.use({
|
|
589
|
+
renderer: {
|
|
590
|
+
paragraph(token) {
|
|
591
|
+
const embeds = this.options[EMBEDS];
|
|
592
|
+
const html = paragraphEmbedHtml(token, embeds);
|
|
593
|
+
return html === void 0 ? false : `${html}
|
|
594
|
+
`;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
return async (markdown, renderOptions = {}) => {
|
|
492
599
|
const source = Array.isArray(markdown) ? markdown.join("\n\n") : markdown ?? "";
|
|
493
600
|
if (!source) return "";
|
|
494
|
-
|
|
601
|
+
const parseOptions = { [EMBEDS]: renderOptions.embeds ?? null };
|
|
602
|
+
return await marked.parse(source, parseOptions);
|
|
495
603
|
};
|
|
496
604
|
}
|
|
497
605
|
function preserveSoftBreaks(source) {
|
|
@@ -543,6 +651,7 @@ function selectValuesForPage(items, pageSlug, count = 4) {
|
|
|
543
651
|
var DEFAULT_IMAGE_ROLES = ["hero", "featured", "card"];
|
|
544
652
|
function createFieldAccessor(entry, context) {
|
|
545
653
|
const title = asString(entry.title);
|
|
654
|
+
const embeds = entryEmbeds(entry);
|
|
546
655
|
const accessor = {
|
|
547
656
|
/** The entry itself, for anything the accessor doesn't cover. */
|
|
548
657
|
entry,
|
|
@@ -605,7 +714,17 @@ function createFieldAccessor(entry, context) {
|
|
|
605
714
|
const raw = field(entry, key) ?? entry.contentMarkdown;
|
|
606
715
|
const source = Array.isArray(raw) ? raw.join("\n\n") : asString(raw);
|
|
607
716
|
if (!source) return void 0;
|
|
608
|
-
return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source
|
|
717
|
+
return context.renderMarkdown(options.softBreaks ? preserveSoftBreaks(source) : source, {
|
|
718
|
+
embeds: options.embeds === false ? void 0 : embeds
|
|
719
|
+
});
|
|
720
|
+
},
|
|
721
|
+
/**
|
|
722
|
+
* The resolved embed for an `embed` field (its value is the provider URL), or `undefined`
|
|
723
|
+
* when the field is empty or the server hasn't resolved it. Render it with `<Embed>` from
|
|
724
|
+
* `@inneropen/marvin-astro/components`, or output `embed.html`.
|
|
725
|
+
*/
|
|
726
|
+
embed(key) {
|
|
727
|
+
return findEmbed(embeds, asString(field(entry, key)));
|
|
609
728
|
},
|
|
610
729
|
/**
|
|
611
730
|
* The entry's primary image. Checks, in order: a hand-authored `metadata_json.featuredImage`,
|
|
@@ -769,7 +888,14 @@ function createRepository(context, options) {
|
|
|
769
888
|
}
|
|
770
889
|
return useFallback();
|
|
771
890
|
}
|
|
891
|
+
function findInList(items, slug) {
|
|
892
|
+
return items.find((item) => slugOf(item) === slug);
|
|
893
|
+
}
|
|
772
894
|
async function loadBySlug(slug) {
|
|
895
|
+
if (allPromise) {
|
|
896
|
+
const listed = findInList(await allPromise, slug);
|
|
897
|
+
if (listed) return listed;
|
|
898
|
+
}
|
|
773
899
|
if (fetcher.backend.hasBackend()) {
|
|
774
900
|
try {
|
|
775
901
|
const entry = await fetcher.entry(slug);
|
|
@@ -779,7 +905,7 @@ function createRepository(context, options) {
|
|
|
779
905
|
fetcher.backend.warn(`Entry "${slug}" failed, using fallback: ${errorMessage(error)}`);
|
|
780
906
|
}
|
|
781
907
|
}
|
|
782
|
-
return (await all()
|
|
908
|
+
return findInList(await all(), slug);
|
|
783
909
|
}
|
|
784
910
|
function all() {
|
|
785
911
|
allPromise ??= load();
|
|
@@ -816,7 +942,7 @@ async function loadSectionLanding(fetcher, slug, renderMarkdown = createMarkdown
|
|
|
816
942
|
const source = asString(entryField(entry, options.bodyField ?? "body")) ?? asString(entry.summary);
|
|
817
943
|
return {
|
|
818
944
|
title: asString(entry.title),
|
|
819
|
-
introHtml: source ? await renderMarkdown(preserveSoftBreaks(source)) : void 0,
|
|
945
|
+
introHtml: source ? await renderMarkdown(preserveSoftBreaks(source), { embeds: entryEmbeds(entry) }) : void 0,
|
|
820
946
|
hero: assetUrl(selectAssetByRole(entry, options.heroRole ?? "hero"))
|
|
821
947
|
};
|
|
822
948
|
}
|
|
@@ -903,6 +1029,10 @@ function transformMarvinSite(marvinSite, fallback) {
|
|
|
903
1029
|
canonicalUrl: asString(config.canonicalUrl),
|
|
904
1030
|
siteName: title
|
|
905
1031
|
}),
|
|
1032
|
+
// Older servers and SDKs don't send it; `siteEmbeds` returns undefined then.
|
|
1033
|
+
embeds: siteEmbeds(
|
|
1034
|
+
config.embeds ?? marvinSite.embeds
|
|
1035
|
+
),
|
|
906
1036
|
metadata
|
|
907
1037
|
};
|
|
908
1038
|
}
|
|
@@ -1009,6 +1139,7 @@ function createMarvinContent(options = {}) {
|
|
|
1009
1139
|
}
|
|
1010
1140
|
export {
|
|
1011
1141
|
DEFAULT_DEV_RETRY_MS,
|
|
1142
|
+
DEFAULT_HYDRATE_CONCURRENCY,
|
|
1012
1143
|
ENV_KEYS,
|
|
1013
1144
|
asNumber,
|
|
1014
1145
|
asRecord,
|
|
@@ -1033,11 +1164,13 @@ export {
|
|
|
1033
1164
|
describeConfig,
|
|
1034
1165
|
entryAssets,
|
|
1035
1166
|
entryData,
|
|
1167
|
+
entryEmbeds,
|
|
1036
1168
|
entryField,
|
|
1037
1169
|
entryMetadata,
|
|
1038
1170
|
entryResources,
|
|
1039
1171
|
errorMessage,
|
|
1040
1172
|
field,
|
|
1173
|
+
findEmbed,
|
|
1041
1174
|
focalPoint,
|
|
1042
1175
|
formatDisplayDate,
|
|
1043
1176
|
isExternalHref,
|
|
@@ -1055,6 +1188,7 @@ export {
|
|
|
1055
1188
|
selectIconAsset,
|
|
1056
1189
|
selectImageAsset,
|
|
1057
1190
|
selectValuesForPage,
|
|
1191
|
+
siteEmbeds,
|
|
1058
1192
|
socialLinkFromKey,
|
|
1059
1193
|
toNavigationLink
|
|
1060
1194
|
};
|