@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.
Files changed (47) hide show
  1. package/README.md +175 -9
  2. package/dist/embeds-BTNryQA6.d.ts +79 -0
  3. package/dist/index.d.ts +42 -3
  4. package/dist/index.js +143 -9
  5. package/dist/index.js.map +1 -1
  6. package/dist/renderers/index.d.ts +119 -0
  7. package/dist/renderers/index.js +251 -0
  8. package/dist/renderers/index.js.map +1 -0
  9. package/dist/types.d.ts +8 -1
  10. package/package.json +16 -6
  11. package/src/chrome.ts +242 -0
  12. package/src/client.ts +119 -0
  13. package/src/components/Embed.astro +119 -0
  14. package/src/components/EmbedLoader.astro +24 -0
  15. package/src/components/EntryRenderer.astro +40 -0
  16. package/src/components/FormRenderer.astro +188 -0
  17. package/src/components/LinkCard.astro +42 -0
  18. package/src/components/embed-frame.ts +168 -0
  19. package/src/components/embeds.css +137 -0
  20. package/src/components/index.ts +29 -0
  21. package/src/components/integration.ts +62 -0
  22. package/src/components/markdown.ts +4 -0
  23. package/src/components/registry.ts +34 -0
  24. package/src/components/renderers/ArticleRenderer.astro +56 -0
  25. package/src/components/renderers/FaqRenderer.astro +26 -0
  26. package/src/components/renderers/NavigationRenderer.astro +27 -0
  27. package/src/components/renderers/PageRenderer.astro +51 -0
  28. package/src/config.ts +149 -0
  29. package/src/embeds.ts +137 -0
  30. package/src/env.d.ts +5 -0
  31. package/src/fetch.ts +198 -0
  32. package/src/fields.ts +330 -0
  33. package/src/format.ts +63 -0
  34. package/src/index.ts +169 -0
  35. package/src/markdown.ts +84 -0
  36. package/src/normalize.ts +284 -0
  37. package/src/renderers/debug.ts +6 -0
  38. package/src/renderers/index.ts +48 -0
  39. package/src/renderers/registry.ts +133 -0
  40. package/src/renderers/resolve.ts +134 -0
  41. package/src/renderers/types.ts +71 -0
  42. package/src/renderers/validation-types.ts +23 -0
  43. package/src/renderers/validation.ts +55 -0
  44. package/src/repository.ts +190 -0
  45. package/src/sections.ts +39 -0
  46. package/src/site.ts +222 -0
  47. 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 Promise.all(
421
- entries.map(async (item) => {
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 entry(item.slug ?? "");
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
- return async (markdown) => {
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
- return await marked.parse(source);
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()).find((item) => slugOf(item) === slug);
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
  };