@ampless/runtime 1.0.0-alpha.55 → 1.0.0-alpha.57
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 +26 -2
- package/dist/index.js +103 -6
- package/package.json +4 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Post, MediaMetadata, AmplessPlugin, ContentFieldRenderer, PluginPublicRenderContext, TiptapNodeMarkdownAdapters, Config, ThemeModule, ThemeManifest, PluginPackageManifest } from 'ampless';
|
|
1
|
+
import { Post, MediaMetadata, AmplessPlugin, ContentFieldRenderer, PluginPublicRenderContext, TiptapNodeHtmlAdapters, TiptapNodeMarkdownAdapters, Config, ThemeModule, ThemeManifest, PluginPackageManifest } from 'ampless';
|
|
2
2
|
export { Config, Post, ThemeManifest } from 'ampless';
|
|
3
3
|
import { Metadata } from 'next';
|
|
4
4
|
import { ReactNode } from 'react';
|
|
@@ -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
|
|
@@ -210,8 +225,17 @@ declare function renderBodyHtmlString(post: Post): string;
|
|
|
210
225
|
* markdown without editing) can still hand us a raw HTML string
|
|
211
226
|
* here. In that case, return it as-is rather than walking it as a
|
|
212
227
|
* malformed tiptap node and producing empty output.
|
|
228
|
+
*
|
|
229
|
+
* `opts.nodeAdapters` lets callers supply per-nodeType serialisers —
|
|
230
|
+
* the admin post-form passes the registry populated by
|
|
231
|
+
* `installAdminTiptapNodeHtml` so that plugin-registered embed nodes
|
|
232
|
+
* (e.g. `amplessYoutube`) are serialised to placeholder div strings
|
|
233
|
+
* instead of falling through with empty children. Callers that omit
|
|
234
|
+
* `opts` get the original behaviour unchanged.
|
|
213
235
|
*/
|
|
214
|
-
declare function tiptapToHtml(doc: unknown
|
|
236
|
+
declare function tiptapToHtml(doc: unknown, opts?: {
|
|
237
|
+
nodeAdapters?: TiptapNodeHtmlAdapters;
|
|
238
|
+
}): string;
|
|
215
239
|
/** Convert markdown to HTML using marked + GFM. */
|
|
216
240
|
declare function markdownToHtml(md: string): string;
|
|
217
241
|
/**
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
259
260
|
}
|
|
@@ -264,7 +265,12 @@ function textAlignStyle(attrs) {
|
|
|
264
265
|
}
|
|
265
266
|
return "";
|
|
266
267
|
}
|
|
267
|
-
function renderTiptapString(node) {
|
|
268
|
+
function renderTiptapString(node, opts) {
|
|
269
|
+
const adapter = opts?.nodeAdapters?.[node.type];
|
|
270
|
+
if (adapter) {
|
|
271
|
+
const out = adapter(node);
|
|
272
|
+
if (typeof out === "string") return out;
|
|
273
|
+
}
|
|
268
274
|
if (node.type === "text") {
|
|
269
275
|
let html = escape(node.text ?? "");
|
|
270
276
|
for (const mark of node.marks ?? []) {
|
|
@@ -281,7 +287,7 @@ function renderTiptapString(node) {
|
|
|
281
287
|
}
|
|
282
288
|
return html;
|
|
283
289
|
}
|
|
284
|
-
const children = (node.content ?? []).map(renderTiptapString).join("");
|
|
290
|
+
const children = (node.content ?? []).map((c) => renderTiptapString(c, opts)).join("");
|
|
285
291
|
switch (node.type) {
|
|
286
292
|
case "doc":
|
|
287
293
|
return children;
|
|
@@ -350,6 +356,7 @@ function renderMarkdownString(md) {
|
|
|
350
356
|
}
|
|
351
357
|
function buildContentFieldRegistry(plugins) {
|
|
352
358
|
const tiptap = /* @__PURE__ */ new Map();
|
|
359
|
+
const htmlPlaceholder = /* @__PURE__ */ new Map();
|
|
353
360
|
const seenMarkdownPatterns = /* @__PURE__ */ new Set();
|
|
354
361
|
const markdownUrl = [];
|
|
355
362
|
for (const plugin of plugins) {
|
|
@@ -363,6 +370,15 @@ function buildContentFieldRegistry(plugins) {
|
|
|
363
370
|
);
|
|
364
371
|
}
|
|
365
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
|
+
}
|
|
366
382
|
} else if (field.kind === "markdown-url") {
|
|
367
383
|
const key = field.pattern.source;
|
|
368
384
|
if (seenMarkdownPatterns.has(key)) {
|
|
@@ -375,7 +391,7 @@ function buildContentFieldRegistry(plugins) {
|
|
|
375
391
|
}
|
|
376
392
|
}
|
|
377
393
|
}
|
|
378
|
-
return { tiptap, markdownUrl };
|
|
394
|
+
return { tiptap, markdownUrl, htmlPlaceholder };
|
|
379
395
|
}
|
|
380
396
|
function htmlPassthrough(html) {
|
|
381
397
|
return createElement("span", { dangerouslySetInnerHTML: { __html: html } });
|
|
@@ -575,9 +591,90 @@ function extractSingleUrl(para) {
|
|
|
575
591
|
}
|
|
576
592
|
return null;
|
|
577
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
|
+
}
|
|
578
675
|
function renderBody(post, opts = {}) {
|
|
579
676
|
if (post.format === "html") {
|
|
580
|
-
return
|
|
677
|
+
return renderHtmlNode(String(post.body), opts);
|
|
581
678
|
}
|
|
582
679
|
if (post.format === "markdown") {
|
|
583
680
|
return renderMarkdownNode(String(post.body), opts);
|
|
@@ -599,9 +696,9 @@ function renderBodyHtmlString(post) {
|
|
|
599
696
|
}
|
|
600
697
|
return "";
|
|
601
698
|
}
|
|
602
|
-
function tiptapToHtml(doc) {
|
|
699
|
+
function tiptapToHtml(doc, opts) {
|
|
603
700
|
if (typeof doc === "string") return doc;
|
|
604
|
-
return renderTiptapString(doc);
|
|
701
|
+
return renderTiptapString(doc, opts);
|
|
605
702
|
}
|
|
606
703
|
function markdownToHtml(md) {
|
|
607
704
|
return renderMarkdownString(md);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ampless/runtime",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.57",
|
|
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.
|
|
54
|
-
"ampless": "1.0.0-alpha.
|
|
54
|
+
"@ampless/plugin-og-image": "0.2.0-alpha.48",
|
|
55
|
+
"ampless": "1.0.0-alpha.48"
|
|
55
56
|
},
|
|
56
57
|
"peerDependencies": {
|
|
57
58
|
"@aws-amplify/adapter-nextjs": "^1",
|