@avocadostudio-ai/preview-adapter 0.1.0 → 0.2.1
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/bridge-functions.d.ts +8 -0
- package/dist/bridge-functions.js +118 -41
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/preview-bridge-core.js +2 -2
- package/package.json +8 -5
|
@@ -7,6 +7,14 @@
|
|
|
7
7
|
* specific transport (postMessage vs direct).
|
|
8
8
|
*/
|
|
9
9
|
export declare function markdownToHtml(md: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Carry the editor-mode parameters from the current URL onto `href`.
|
|
12
|
+
*
|
|
13
|
+
* A parameter the link sets itself always wins — the link is the more specific
|
|
14
|
+
* intent. Everything else about `href` is preserved, so this is safe to apply
|
|
15
|
+
* to any same-origin destination.
|
|
16
|
+
*/
|
|
17
|
+
export declare function withPreviewParams(href: string, currentSearch: string): string;
|
|
10
18
|
export declare function findBlockNode(blockId: string): HTMLElement | null;
|
|
11
19
|
export declare function findEditableNode(parent: HTMLElement, editablePath: string): HTMLElement | null;
|
|
12
20
|
/**
|
package/dist/bridge-functions.js
CHANGED
|
@@ -6,58 +6,133 @@
|
|
|
6
6
|
* communicate back to the editor through injectable callbacks, decoupled from any
|
|
7
7
|
* specific transport (postMessage vs direct).
|
|
8
8
|
*/
|
|
9
|
-
import { isImagePath } from "@avocadostudio-ai/shared";
|
|
9
|
+
import { isImagePath, parseInline, parseRichTextBlocks, normalizeRichTextBody, resolveRichTextHeadingLevel } from "@avocadostudio-ai/shared";
|
|
10
10
|
// ---------------------------------------------------------------------------
|
|
11
|
-
// Markdown → HTML
|
|
11
|
+
// Markdown → HTML for the live-draft overlay
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
//
|
|
14
|
+
// The grammar comes from @avocadostudio-ai/shared, the same parser the block
|
|
15
|
+
// renderers use. Two things are specific to this surface and stay here:
|
|
16
|
+
//
|
|
17
|
+
// 1. we escape `& < >` ourselves — React does that for the renderers, but
|
|
18
|
+
// this output is written straight into `innerHTML`. The escaping happens
|
|
19
|
+
// per token, on the way out. Doing it up front on the whole string (as
|
|
20
|
+
// this did originally) turns `>` into `>` before the parser sees it,
|
|
21
|
+
// so a blockquote can never match;
|
|
22
|
+
// 2. a single-line value returns bare inline HTML with no `<p>` wrapper,
|
|
23
|
+
// because it is written into an element that already exists.
|
|
24
|
+
//
|
|
25
|
+
// Note this only ever runs on *plain-text* fields: `applyLiveDraftFields`
|
|
26
|
+
// hands anything structurally rich back to React (see `isRichEditableNode`).
|
|
27
|
+
function escapeHtml(text) {
|
|
28
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
29
|
+
}
|
|
30
|
+
/** Attribute values additionally need the quote that delimits them. */
|
|
31
|
+
function escapeAttribute(value) {
|
|
32
|
+
return escapeHtml(value).replace(/"/g, """);
|
|
33
|
+
}
|
|
34
|
+
function inlineToHtml(tokens) {
|
|
35
|
+
return tokens
|
|
36
|
+
.map((token) => {
|
|
37
|
+
switch (token.type) {
|
|
38
|
+
case "strong":
|
|
39
|
+
return `<strong>${escapeHtml(token.text)}</strong>`;
|
|
40
|
+
case "em":
|
|
41
|
+
return `<em>${escapeHtml(token.text)}</em>`;
|
|
42
|
+
case "strike":
|
|
43
|
+
return `<s>${escapeHtml(token.text)}</s>`;
|
|
44
|
+
case "code":
|
|
45
|
+
return `<code>${escapeHtml(token.text)}</code>`;
|
|
46
|
+
case "link":
|
|
47
|
+
return `<a href="${escapeAttribute(token.href)}">${escapeHtml(token.text)}</a>`;
|
|
48
|
+
case "break":
|
|
49
|
+
return "<br>";
|
|
50
|
+
default:
|
|
51
|
+
return escapeHtml(token.text);
|
|
52
|
+
}
|
|
53
|
+
})
|
|
54
|
+
.join("");
|
|
18
55
|
}
|
|
19
56
|
export function markdownToHtml(md) {
|
|
20
|
-
const
|
|
21
|
-
.replace(/&/g, "&")
|
|
22
|
-
.replace(/</g, "<")
|
|
23
|
-
.replace(/>/g, ">");
|
|
24
|
-
const normalized = escaped
|
|
25
|
-
.replace(/\r\n?/g, "\n")
|
|
26
|
-
.replace(/([.!?])([A-Z])/g, "$1 $2")
|
|
27
|
-
.replace(/\n{3,}/g, "\n\n")
|
|
28
|
-
.trim();
|
|
57
|
+
const normalized = normalizeRichTextBody(md);
|
|
29
58
|
if (!normalized.includes("\n")) {
|
|
30
|
-
return inlineToHtml(normalized);
|
|
59
|
+
return inlineToHtml(parseInline(normalized));
|
|
31
60
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
html += `<p>${inlineToHtml(rest)}</p>`;
|
|
44
|
-
return html;
|
|
45
|
-
}
|
|
46
|
-
const ulItems = lines.map((l) => /^\s*[-*+•]\s+(.+)$/.exec(l)?.[1]?.trim() ?? null);
|
|
47
|
-
if (ulItems.every((i) => i !== null)) {
|
|
48
|
-
return `<ul>${ulItems.map((i) => `<li>${inlineToHtml(i)}</li>`).join("")}</ul>`;
|
|
49
|
-
}
|
|
50
|
-
const olItems = lines.map((l) => /^\s*\d+[.)]\s+(.+)$/.exec(l)?.[1]?.trim() ?? null);
|
|
51
|
-
if (olItems.every((i) => i !== null)) {
|
|
52
|
-
return `<ol>${olItems.map((i) => `<li>${inlineToHtml(i)}</li>`).join("")}</ol>`;
|
|
53
|
-
}
|
|
54
|
-
return `<p>${inlineToHtml(block)}</p>`;
|
|
61
|
+
return parseRichTextBlocks(normalized).map(blockToHtml).join("");
|
|
62
|
+
}
|
|
63
|
+
function itemsToHtml(items) {
|
|
64
|
+
return items
|
|
65
|
+
.map((item) => {
|
|
66
|
+
const nested = item.children
|
|
67
|
+
? item.children.type === "ordered-list"
|
|
68
|
+
? `<ol>${itemsToHtml(item.children.items)}</ol>`
|
|
69
|
+
: `<ul>${itemsToHtml(item.children.items)}</ul>`
|
|
70
|
+
: "";
|
|
71
|
+
return `<li>${inlineToHtml(item.inline)}${nested}</li>`;
|
|
55
72
|
})
|
|
56
73
|
.join("");
|
|
57
74
|
}
|
|
75
|
+
function blockToHtml(block) {
|
|
76
|
+
switch (block.type) {
|
|
77
|
+
case "heading": {
|
|
78
|
+
const level = resolveRichTextHeadingLevel(block.level);
|
|
79
|
+
const heading = `<h${level}>${inlineToHtml(block.inline)}</h${level}>`;
|
|
80
|
+
return block.trailing ? `${heading}<p>${inlineToHtml(block.trailing)}</p>` : heading;
|
|
81
|
+
}
|
|
82
|
+
case "unordered-list":
|
|
83
|
+
return `<ul>${itemsToHtml(block.items)}</ul>`;
|
|
84
|
+
case "ordered-list":
|
|
85
|
+
return `<ol>${itemsToHtml(block.items)}</ol>`;
|
|
86
|
+
case "blockquote":
|
|
87
|
+
return `<blockquote>${block.children.map(blockToHtml).join("")}</blockquote>`;
|
|
88
|
+
case "code": {
|
|
89
|
+
const cls = block.language ? ` class="language-${escapeAttribute(block.language)}"` : "";
|
|
90
|
+
return `<pre><code${cls}>${escapeHtml(block.code)}</code></pre>`;
|
|
91
|
+
}
|
|
92
|
+
case "rule":
|
|
93
|
+
return "<hr>";
|
|
94
|
+
default:
|
|
95
|
+
return `<p>${inlineToHtml(block.inline)}</p>`;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
58
98
|
// ---------------------------------------------------------------------------
|
|
59
99
|
// Pure DOM queries
|
|
60
100
|
// ---------------------------------------------------------------------------
|
|
101
|
+
/**
|
|
102
|
+
* The query parameters that put the site into editor mode.
|
|
103
|
+
*
|
|
104
|
+
* `__editor=1` is the one the site's middleware rewrites onto its preview
|
|
105
|
+
* route; the rest identify the session and authorise reading unpublished
|
|
106
|
+
* content. They ride on the URL the editor frames, and a navigation *inside*
|
|
107
|
+
* the preview has to carry them forward or the very next document is the
|
|
108
|
+
* ordinary public page — published content, no overlay, and a hard 404 for any
|
|
109
|
+
* page that has never been published.
|
|
110
|
+
*/
|
|
111
|
+
const PREVIEW_PARAM_KEYS = ["__editor", "session", "siteId", "editorOrigin", "secret"];
|
|
112
|
+
/**
|
|
113
|
+
* Carry the editor-mode parameters from the current URL onto `href`.
|
|
114
|
+
*
|
|
115
|
+
* A parameter the link sets itself always wins — the link is the more specific
|
|
116
|
+
* intent. Everything else about `href` is preserved, so this is safe to apply
|
|
117
|
+
* to any same-origin destination.
|
|
118
|
+
*/
|
|
119
|
+
export function withPreviewParams(href, currentSearch) {
|
|
120
|
+
try {
|
|
121
|
+
const base = typeof window !== "undefined" ? window.location.href : "http://localhost/";
|
|
122
|
+
const url = new URL(href, base);
|
|
123
|
+
const current = new URLSearchParams(currentSearch);
|
|
124
|
+
for (const key of PREVIEW_PARAM_KEYS) {
|
|
125
|
+
const value = current.get(key);
|
|
126
|
+
if (value !== null && !url.searchParams.has(key))
|
|
127
|
+
url.searchParams.set(key, value);
|
|
128
|
+
}
|
|
129
|
+
return url.pathname + url.search + url.hash;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Malformed href — better to navigate somewhere than to throw mid-click.
|
|
133
|
+
return href;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
61
136
|
export function findBlockNode(blockId) {
|
|
62
137
|
if (!blockId)
|
|
63
138
|
return null;
|
|
@@ -1271,7 +1346,9 @@ export function createBridgeFunctions(state, callbacks, config) {
|
|
|
1271
1346
|
if (url.origin === window.location.origin) {
|
|
1272
1347
|
event.preventDefault();
|
|
1273
1348
|
event.stopPropagation();
|
|
1274
|
-
|
|
1349
|
+
// Without this the click leaves editor mode: the site's own links
|
|
1350
|
+
// carry no `__editor=1`, so the next document is the public page.
|
|
1351
|
+
navigate(withPreviewParams(url.pathname + url.search + url.hash, window.location.search));
|
|
1275
1352
|
return;
|
|
1276
1353
|
}
|
|
1277
1354
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { PreviewBridge } from "./preview-bridge.tsx";
|
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.tsx";
|
|
3
3
|
export type { PreviewBridgeConfig, PreviewBridgeCoreProps } from "./preview-bridge-core.tsx";
|
|
4
4
|
export { getPreviewWrapperProps } from "./selectable.ts";
|
|
5
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.ts";
|
|
5
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.ts";
|
|
6
6
|
export type { BridgeCallbacks, BridgeState, BridgeFunctions } from "./bridge-functions.ts";
|
|
7
7
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.tsx";
|
|
8
8
|
export type { LivePreviewPage, LivePreviewBridgeApi } from "./live-preview-store.tsx";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { PreviewBridge } from "./preview-bridge.js";
|
|
2
2
|
export { PreviewBridgeCore } from "./preview-bridge-core.js";
|
|
3
3
|
export { getPreviewWrapperProps } from "./selectable.js";
|
|
4
|
-
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.js";
|
|
4
|
+
export { createBridgeFunctions, createBridgeState, findBlockNode, findEditableNode, parseListItemPath, supportsInlineEditablePath, readNodeText, placeCaretAtEnd, orderedBlockNodes, blockOrderIndex, computeMoveAfter, computeInsertBefore, groupListItemNodes, commonItemRoot, markdownToHtml, withPreviewParams, setNestedLabelsVisibility, clearChildFocus, clearListItemSelection, removeOverlayControls, clearAllHighlights, showSkeleton, removeSkeletons, ensureBlockBadges, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.js";
|
|
5
5
|
export { LivePreviewProvider, useLivePreviewBlocks, useLivePreviewBridgeApi, } from "./live-preview-store.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
|
-
import { createBridgeFunctions, createBridgeState, findBlockNode, parseListItemPath, ensureBlockBadges, setNestedLabelsVisibility, showSkeleton, removeSkeletons, clearChildFocus, clearAllHighlights, clearListItemSelection, applyAiFieldLoading, cleanupOverlayElements, } from "./bridge-functions.js";
|
|
4
|
+
import { createBridgeFunctions, createBridgeState, findBlockNode, parseListItemPath, ensureBlockBadges, setNestedLabelsVisibility, showSkeleton, removeSkeletons, clearChildFocus, clearAllHighlights, clearListItemSelection, applyAiFieldLoading, cleanupOverlayElements, withPreviewParams, } from "./bridge-functions.js";
|
|
5
5
|
export function PreviewBridgeCore(props) {
|
|
6
6
|
// When running standalone (no editor origin) or not embedded in an iframe, render nothing.
|
|
7
7
|
if (!props.editorOrigin || typeof window !== "undefined" && window.parent === window)
|
|
@@ -154,7 +154,7 @@ function PreviewBridgeCoreInner({ slug, editorOrigin, navigate, refresh, pathnam
|
|
|
154
154
|
const navigateTo = typeof msg.payload.navigateTo === "string" ? msg.payload.navigateTo.trim() : "";
|
|
155
155
|
if (navigateTo) {
|
|
156
156
|
const href = navigateTo.startsWith("/") ? navigateTo : `/${navigateTo}`;
|
|
157
|
-
navigate(
|
|
157
|
+
navigate(withPreviewParams(href, window.location.search));
|
|
158
158
|
}
|
|
159
159
|
else {
|
|
160
160
|
bridge.smoothRefresh();
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avocadostudio-ai/preview-adapter",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
7
|
"exports": {
|
|
8
8
|
"./package.json": "./package.json",
|
|
9
9
|
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
10
11
|
"import": "./dist/index.js",
|
|
11
|
-
"
|
|
12
|
+
"default": "./dist/index.js"
|
|
12
13
|
},
|
|
13
14
|
"./core": {
|
|
15
|
+
"types": "./dist/preview-bridge-core.d.ts",
|
|
14
16
|
"import": "./dist/preview-bridge-core.js",
|
|
15
|
-
"
|
|
17
|
+
"default": "./dist/preview-bridge-core.js"
|
|
16
18
|
},
|
|
17
19
|
"./styles.css": "./src/styles.css"
|
|
18
20
|
},
|
|
@@ -30,7 +32,7 @@
|
|
|
30
32
|
"src/styles.css"
|
|
31
33
|
],
|
|
32
34
|
"dependencies": {
|
|
33
|
-
"@avocadostudio-ai/shared": "0.1
|
|
35
|
+
"@avocadostudio-ai/shared": "0.2.1"
|
|
34
36
|
},
|
|
35
37
|
"peerDependencies": {
|
|
36
38
|
"next": ">=15.0.0",
|
|
@@ -43,6 +45,7 @@
|
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
47
|
"@types/react": "^19.0.10",
|
|
48
|
+
"tsx": "^4.21.0",
|
|
46
49
|
"typescript": "^5.7.3"
|
|
47
50
|
},
|
|
48
51
|
"description": "Preview bridge and editor overlay for Avocado Studio live preview",
|
|
@@ -54,6 +57,6 @@
|
|
|
54
57
|
"scripts": {
|
|
55
58
|
"build": "tsc -p tsconfig.build.json",
|
|
56
59
|
"typecheck": "tsc --noEmit",
|
|
57
|
-
"test": "NODE_ENV=test
|
|
60
|
+
"test": "NODE_ENV=test node ../../scripts/run-tests.mjs"
|
|
58
61
|
}
|
|
59
62
|
}
|