@ampless/runtime 1.0.0-alpha.56 → 1.0.0-alpha.58

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/dist/index.d.ts CHANGED
@@ -165,6 +165,21 @@ interface ContentFieldRegistry {
165
165
  kind: 'markdown-url';
166
166
  }>;
167
167
  }>;
168
+ /**
169
+ * Plugins that declared `htmlPlaceholder` on their `kind: 'tiptap'`
170
+ * entry, keyed by the **lowercased** `flagAttr` (htmlparser2 lowercases
171
+ * HTML attribute names while parsing, so the public html walker looks
172
+ * up `attribs[flagAttr.toLowerCase()]` and compares against these keys
173
+ * case-insensitively). Used by `renderHtmlNode` to expand canonical
174
+ * placeholder divs in `format: 'html'` bodies into the plugin's
175
+ * existing tiptap renderer output.
176
+ */
177
+ htmlPlaceholder: ReadonlyMap<string, {
178
+ plugin: AmplessPlugin;
179
+ renderer: Extract<ContentFieldRenderer, {
180
+ kind: 'tiptap';
181
+ }>;
182
+ }>;
168
183
  }
169
184
  /**
170
185
  * Build a `ContentFieldRegistry` from a list of plugins. Eagerly errors
package/dist/index.js CHANGED
@@ -254,6 +254,7 @@ import {
254
254
  createElement
255
255
  } from "react";
256
256
  import { marked } from "marked";
257
+ import { parseDocument, ElementType } from "htmlparser2";
257
258
  function escape(s) {
258
259
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
259
260
  }
@@ -355,6 +356,7 @@ function renderMarkdownString(md) {
355
356
  }
356
357
  function buildContentFieldRegistry(plugins) {
357
358
  const tiptap = /* @__PURE__ */ new Map();
359
+ const htmlPlaceholder = /* @__PURE__ */ new Map();
358
360
  const seenMarkdownPatterns = /* @__PURE__ */ new Set();
359
361
  const markdownUrl = [];
360
362
  for (const plugin of plugins) {
@@ -368,6 +370,15 @@ function buildContentFieldRegistry(plugins) {
368
370
  );
369
371
  }
370
372
  tiptap.set(field.nodeType, { plugin, renderer: field });
373
+ if (field.htmlPlaceholder) {
374
+ const flagAttr = field.htmlPlaceholder.flagAttr.toLowerCase();
375
+ if (htmlPlaceholder.has(flagAttr)) {
376
+ throw new Error(
377
+ `[ampless contentFields] duplicate htmlPlaceholder flagAttr "${flagAttr}" \u2014 already registered by another plugin. Each flagAttr may be claimed by at most one plugin.`
378
+ );
379
+ }
380
+ htmlPlaceholder.set(flagAttr, { plugin, renderer: field });
381
+ }
371
382
  } else if (field.kind === "markdown-url") {
372
383
  const key = field.pattern.source;
373
384
  if (seenMarkdownPatterns.has(key)) {
@@ -380,7 +391,7 @@ function buildContentFieldRegistry(plugins) {
380
391
  }
381
392
  }
382
393
  }
383
- return { tiptap, markdownUrl };
394
+ return { tiptap, markdownUrl, htmlPlaceholder };
384
395
  }
385
396
  function htmlPassthrough(html) {
386
397
  return createElement("span", { dangerouslySetInnerHTML: { __html: html } });
@@ -580,9 +591,90 @@ function extractSingleUrl(para) {
580
591
  }
581
592
  return null;
582
593
  }
594
+ function isParsedElement(node) {
595
+ return ElementType.isTag(node);
596
+ }
597
+ function renderHtmlNode(html, opts) {
598
+ const registry = opts.contentFields?.htmlPlaceholder;
599
+ if (!registry || registry.size === 0) {
600
+ return htmlPassthroughBlock(html);
601
+ }
602
+ const lower = html.toLowerCase();
603
+ let anyFlagPresent = false;
604
+ for (const flagAttr of registry.keys()) {
605
+ if (lower.includes(flagAttr)) {
606
+ anyFlagPresent = true;
607
+ break;
608
+ }
609
+ }
610
+ if (!anyFlagPresent) {
611
+ return htmlPassthroughBlock(html);
612
+ }
613
+ let doc;
614
+ try {
615
+ doc = parseDocument(html, {
616
+ withStartIndices: true,
617
+ withEndIndices: true
618
+ });
619
+ } catch (err) {
620
+ console.warn(
621
+ `[ampless renderBody] htmlparser2 failed to parse a format:'html' body; falling back to raw passthrough: ${err instanceof Error ? err.message : String(err)}`
622
+ );
623
+ return htmlPassthroughBlock(html);
624
+ }
625
+ const children = [];
626
+ let chunk = 0;
627
+ let cursor = 0;
628
+ const flushRawUntil = (end) => {
629
+ if (end <= cursor) return;
630
+ const slice = html.slice(cursor, end);
631
+ children.push(htmlPassthroughBlock(slice, `html-raw-${chunk++}`));
632
+ cursor = end;
633
+ };
634
+ for (const node of doc.children) {
635
+ if (!isParsedElement(node)) continue;
636
+ if (node.startIndex == null || node.endIndex == null) continue;
637
+ const reg = matchHtmlPlaceholder(node.attribs, registry);
638
+ if (!reg) continue;
639
+ const start = node.startIndex;
640
+ const endExclusive = node.endIndex + 1;
641
+ flushRawUntil(start);
642
+ const placeholderSlice = html.slice(start, endExclusive);
643
+ const ctx = opts.ctxForPlugin?.(reg.plugin);
644
+ if (!ctx) {
645
+ children.push(htmlPassthroughBlock(placeholderSlice, `html-raw-${chunk++}`));
646
+ cursor = endExclusive;
647
+ continue;
648
+ }
649
+ try {
650
+ const renderNode = {
651
+ type: reg.renderer.nodeType,
652
+ attrs: reg.renderer.htmlPlaceholder.attrsFromElement(node.attribs)
653
+ };
654
+ const out = reg.renderer.render(renderNode, ctx);
655
+ children.push(createElement(Fragment, { key: `html-embed-${start}` }, out));
656
+ } catch (err) {
657
+ console.warn(
658
+ `[ampless renderBody] plugin "${reg.plugin.instanceId ?? reg.plugin.name}" threw inside contentFields tiptap renderer for nodeType "${reg.renderer.nodeType}": ${err instanceof Error ? err.message : String(err)}`
659
+ );
660
+ children.push(htmlPassthroughBlock(placeholderSlice, `html-raw-${chunk++}`));
661
+ }
662
+ cursor = endExclusive;
663
+ }
664
+ flushRawUntil(html.length);
665
+ return createElement(Fragment, null, ...children);
666
+ }
667
+ function matchHtmlPlaceholder(attribs, registry) {
668
+ for (const flagAttr of registry.keys()) {
669
+ if (Object.prototype.hasOwnProperty.call(attribs, flagAttr)) {
670
+ return registry.get(flagAttr) ?? null;
671
+ }
672
+ }
673
+ return null;
674
+ }
583
675
  function renderBody(post, opts = {}) {
584
676
  if (post.format === "html") {
585
- return htmlPassthroughBlock(String(post.body));
677
+ return renderHtmlNode(String(post.body), opts);
586
678
  }
587
679
  if (post.format === "markdown") {
588
680
  return renderMarkdownNode(String(post.body), opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ampless/runtime",
3
- "version": "1.0.0-alpha.56",
3
+ "version": "1.0.0-alpha.58",
4
4
  "description": "Public-side runtime for ampless: post fetching, theme dispatch, middleware, public route handlers",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -46,12 +46,13 @@
46
46
  "@radix-ui/react-slot": "^1.2.4",
47
47
  "class-variance-authority": "^0.7.1",
48
48
  "clsx": "^2.1.1",
49
+ "htmlparser2": "^10.1.0",
49
50
  "lucide-react": "^1.16.0",
50
51
  "marked": "^18.0.4",
51
52
  "sanitize-html": "^2.17.4",
52
53
  "tailwind-merge": "^3.6.0",
53
- "@ampless/plugin-og-image": "0.2.0-alpha.47",
54
- "ampless": "1.0.0-alpha.47"
54
+ "@ampless/plugin-og-image": "0.2.0-alpha.49",
55
+ "ampless": "1.0.0-alpha.49"
55
56
  },
56
57
  "peerDependencies": {
57
58
  "@aws-amplify/adapter-nextjs": "^1",