@wocart/theme 1.3.0 → 1.5.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/CHANGELOG.md +73 -0
- package/README.md +315 -17
- package/dist/astro/InstanceView.astro +21 -5
- package/dist/astro/index.d.ts +15 -14
- package/dist/astro/index.js +44 -0
- package/dist/composition.d.ts +17 -0
- package/dist/composition.js +60 -0
- package/dist/definitions.js +305 -6
- package/dist/image-service.d.ts +39 -0
- package/dist/image-service.js +73 -0
- package/dist/index.d.ts +18 -5
- package/dist/index.js +12 -4
- package/dist/instances.d.ts +7 -1
- package/dist/instances.js +251 -13
- package/dist/manifest-diff.d.ts +14 -0
- package/dist/manifest-diff.js +160 -0
- package/dist/manifest.d.ts +192 -19
- package/dist/manifest.js +77 -16
- package/dist/operations.d.ts +71 -0
- package/dist/operations.js +377 -0
- package/dist/page-data.js +29 -2
- package/dist/preview-bridge.d.ts +28 -0
- package/dist/preview-bridge.js +317 -0
- package/dist/preview-protocol.d.ts +100 -0
- package/dist/preview-protocol.js +51 -0
- package/dist/rich-text.d.ts +31 -0
- package/dist/rich-text.js +233 -0
- package/dist/schemas.d.ts +21 -0
- package/dist/schemas.js +35 -2
- package/dist/types.d.ts +259 -15
- package/dist/version.d.ts +7 -0
- package/dist/version.js +7 -0
- package/package.json +12 -3
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/// <reference lib="dom" />
|
|
2
|
+
/**
|
|
3
|
+
* The preview page's side of the preview protocol: a small browser module a
|
|
4
|
+
* theme loads on its preview route only. It finds the instances the renderer
|
|
5
|
+
* marked, tells the editor where they are and which one the merchant points
|
|
6
|
+
* at, and re-renders a draft the editor sends by posting it back to the
|
|
7
|
+
* storefront and swapping in only the sections that changed — or, when the
|
|
8
|
+
* page around them changed, the whole page, the way Astro's router swaps one.
|
|
9
|
+
*
|
|
10
|
+
* It draws nothing — outlines, toolbars and panels are the editor's. It posts
|
|
11
|
+
* only to the editor origin it is given and reads only that origin's messages
|
|
12
|
+
* from the framing window.
|
|
13
|
+
*/
|
|
14
|
+
import { PREVIEW_PROTOCOL_VERSION, THEME_PREVIEW_PATH, parsePreviewMarker, } from "./preview-protocol.js";
|
|
15
|
+
/**
|
|
16
|
+
* Dispatched on `document` for each section the bridge swapped in, with the
|
|
17
|
+
* instance id as `detail.id`: a section with browser behavior binds it again.
|
|
18
|
+
* When the whole page was replaced instead, the bridge dispatches Astro's own
|
|
19
|
+
* `astro:page-load`, as Astro's router does after it swaps a page.
|
|
20
|
+
*/
|
|
21
|
+
export const PREVIEW_SECTION_LOAD_EVENT = "wocart:section-load";
|
|
22
|
+
const ASTRO_PAGE_LOAD_EVENT = "astro:page-load";
|
|
23
|
+
const SHOW_COMMENT = 0x80;
|
|
24
|
+
const ELEMENT_NODE = 1;
|
|
25
|
+
const COMMENT_NODE = 8;
|
|
26
|
+
/**
|
|
27
|
+
* Every complete marker pair under `root`, in document order, each with the
|
|
28
|
+
* pair that encloses it. Pairs nest or are ignored: a start still open when an
|
|
29
|
+
* enclosing pair closes never pairs.
|
|
30
|
+
*/
|
|
31
|
+
function collect(root) {
|
|
32
|
+
const pairs = [];
|
|
33
|
+
const open = [];
|
|
34
|
+
const walker = (root.ownerDocument ?? root).createTreeWalker(root, SHOW_COMMENT);
|
|
35
|
+
let position = 0;
|
|
36
|
+
for (let node = walker.nextNode(); node !== null; node = walker.nextNode()) {
|
|
37
|
+
const marker = parsePreviewMarker(node.data);
|
|
38
|
+
if (marker === null)
|
|
39
|
+
continue;
|
|
40
|
+
position += 1;
|
|
41
|
+
if (marker.kind === "start") {
|
|
42
|
+
open.push({ id: marker.id, type: marker.type, hidden: marker.hidden, start: node, opened: position });
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const index = open.map((entry) => entry.id).lastIndexOf(marker.id);
|
|
46
|
+
if (index === -1)
|
|
47
|
+
continue;
|
|
48
|
+
pairs.push({ ...open.splice(index)[0], end: node, closed: position });
|
|
49
|
+
}
|
|
50
|
+
pairs.sort((a, b) => a.opened - b.opened);
|
|
51
|
+
return pairs.map(({ opened, closed, ...entry }) => {
|
|
52
|
+
const enclosing = pairs.filter((other) => other.opened < opened && closed < other.closed);
|
|
53
|
+
return { ...entry, parent: enclosing.at(-1)?.id ?? null, depth: enclosing.length };
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
/** The nodes between a pair of markers, or `null` when the pair are not siblings. */
|
|
57
|
+
function between(entry) {
|
|
58
|
+
const nodes = [];
|
|
59
|
+
for (let node = entry.start.nextSibling; node !== entry.end; node = node.nextSibling) {
|
|
60
|
+
if (node === null)
|
|
61
|
+
return null;
|
|
62
|
+
nodes.push(node);
|
|
63
|
+
}
|
|
64
|
+
return nodes;
|
|
65
|
+
}
|
|
66
|
+
function markup(nodes) {
|
|
67
|
+
return nodes
|
|
68
|
+
.map((node) => {
|
|
69
|
+
if (node.nodeType === ELEMENT_NODE)
|
|
70
|
+
return node.outerHTML;
|
|
71
|
+
if (node.nodeType === COMMENT_NODE)
|
|
72
|
+
return `<!--${node.data}-->`;
|
|
73
|
+
return node.textContent ?? "";
|
|
74
|
+
})
|
|
75
|
+
.join("");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The page with every section's content left out — the root's attributes and
|
|
79
|
+
* the body around the sections: what must not change for sections to be
|
|
80
|
+
* swapped alone.
|
|
81
|
+
*/
|
|
82
|
+
function frameOf(doc) {
|
|
83
|
+
const copy = doc.body.cloneNode(true);
|
|
84
|
+
for (const entry of collect(copy)) {
|
|
85
|
+
if (entry.parent !== null)
|
|
86
|
+
continue;
|
|
87
|
+
const nodes = between(entry);
|
|
88
|
+
if (nodes === null)
|
|
89
|
+
return null;
|
|
90
|
+
for (const node of nodes)
|
|
91
|
+
node.parentNode?.removeChild(node);
|
|
92
|
+
}
|
|
93
|
+
const root = doc.documentElement;
|
|
94
|
+
const attributes = root.getAttributeNames().map((name) => `${name}=${JSON.stringify(root.getAttribute(name))}`);
|
|
95
|
+
return `${attributes.join(" ")}\n${copy.outerHTML}`;
|
|
96
|
+
}
|
|
97
|
+
function stylesOf(doc) {
|
|
98
|
+
return Array.from(doc.head.querySelectorAll('style, link[rel="stylesheet"]'));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Starts the bridge on a preview page. It posts `ready` with every marked
|
|
102
|
+
* instance, `layout` as the page scrolls or resizes, `hover` as the merchant
|
|
103
|
+
* points, `select` as they click (in `edit` mode, where links and forms do
|
|
104
|
+
* nothing), and `rendered` after each draft it swaps in.
|
|
105
|
+
*/
|
|
106
|
+
export function startPreviewBridge(options) {
|
|
107
|
+
const win = (options.window ?? window);
|
|
108
|
+
const doc = win.document;
|
|
109
|
+
const endpoint = options.endpoint ?? THEME_PREVIEW_PATH;
|
|
110
|
+
let token = options.token;
|
|
111
|
+
let mode = "edit";
|
|
112
|
+
let marked = collect(doc.body);
|
|
113
|
+
let hovered = null;
|
|
114
|
+
let frame = 0;
|
|
115
|
+
let latest = 0;
|
|
116
|
+
let stopped = false;
|
|
117
|
+
const post = (message) => win.parent.postMessage({ source: "wocart-preview", version: PREVIEW_PROTOCOL_VERSION, ...message }, options.editorOrigin);
|
|
118
|
+
const rangeOf = (entry) => {
|
|
119
|
+
const range = doc.createRange();
|
|
120
|
+
range.setStartAfter(entry.start);
|
|
121
|
+
range.setEndBefore(entry.end);
|
|
122
|
+
return range;
|
|
123
|
+
};
|
|
124
|
+
const instances = () => marked.map((entry) => {
|
|
125
|
+
const box = rangeOf(entry).getBoundingClientRect();
|
|
126
|
+
const rect = box.width > 0 || box.height > 0 ? { x: box.x, y: box.y, width: box.width, height: box.height } : null;
|
|
127
|
+
return { id: entry.id, type: entry.type, parent: entry.parent, hidden: entry.hidden, rect };
|
|
128
|
+
});
|
|
129
|
+
const innermostAt = (target) => {
|
|
130
|
+
if (!(target instanceof win.Node))
|
|
131
|
+
return null;
|
|
132
|
+
let best;
|
|
133
|
+
for (const entry of marked) {
|
|
134
|
+
if (best !== undefined && entry.depth <= best.depth)
|
|
135
|
+
continue;
|
|
136
|
+
if (between(entry)?.some((node) => node.contains(target)))
|
|
137
|
+
best = entry;
|
|
138
|
+
}
|
|
139
|
+
return best?.id ?? null;
|
|
140
|
+
};
|
|
141
|
+
const reportLayout = () => {
|
|
142
|
+
if (frame !== 0)
|
|
143
|
+
return;
|
|
144
|
+
frame = win.requestAnimationFrame(() => {
|
|
145
|
+
frame = 0;
|
|
146
|
+
post({ type: "layout", instances: instances() });
|
|
147
|
+
});
|
|
148
|
+
};
|
|
149
|
+
const onClick = (event) => {
|
|
150
|
+
if (mode !== "edit")
|
|
151
|
+
return;
|
|
152
|
+
event.preventDefault();
|
|
153
|
+
event.stopPropagation();
|
|
154
|
+
const id = innermostAt(event.target);
|
|
155
|
+
if (id !== null)
|
|
156
|
+
post({ type: "select", id });
|
|
157
|
+
};
|
|
158
|
+
const onSubmit = (event) => {
|
|
159
|
+
if (mode !== "edit")
|
|
160
|
+
return;
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
event.stopPropagation();
|
|
163
|
+
};
|
|
164
|
+
const onOver = (event) => {
|
|
165
|
+
const id = innermostAt(event.target);
|
|
166
|
+
if (id === hovered)
|
|
167
|
+
return;
|
|
168
|
+
hovered = id;
|
|
169
|
+
post({ type: "hover", id });
|
|
170
|
+
};
|
|
171
|
+
const onLeave = () => {
|
|
172
|
+
if (hovered === null)
|
|
173
|
+
return;
|
|
174
|
+
hovered = null;
|
|
175
|
+
post({ type: "hover", id: null });
|
|
176
|
+
};
|
|
177
|
+
const swapHead = (next) => {
|
|
178
|
+
const current = stylesOf(doc);
|
|
179
|
+
const wanted = stylesOf(next);
|
|
180
|
+
if (current.length === wanted.length) {
|
|
181
|
+
current.forEach((node, index) => {
|
|
182
|
+
if (node.outerHTML !== wanted[index].outerHTML)
|
|
183
|
+
node.replaceWith(doc.importNode(wanted[index], true));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
for (const node of current)
|
|
188
|
+
node.remove();
|
|
189
|
+
for (const node of wanted)
|
|
190
|
+
doc.head.appendChild(doc.importNode(node, true));
|
|
191
|
+
}
|
|
192
|
+
if (next.title !== doc.title)
|
|
193
|
+
doc.title = next.title;
|
|
194
|
+
};
|
|
195
|
+
const swapPage = (next) => {
|
|
196
|
+
const root = doc.documentElement;
|
|
197
|
+
for (const name of root.getAttributeNames())
|
|
198
|
+
if (!next.documentElement.hasAttribute(name))
|
|
199
|
+
root.removeAttribute(name);
|
|
200
|
+
for (const name of next.documentElement.getAttributeNames())
|
|
201
|
+
root.setAttribute(name, next.documentElement.getAttribute(name));
|
|
202
|
+
const { scrollX, scrollY } = win;
|
|
203
|
+
doc.body.replaceWith(doc.importNode(next.body, true));
|
|
204
|
+
win.scrollTo(scrollX, scrollY);
|
|
205
|
+
};
|
|
206
|
+
/** Swaps `next` in; `null` when the whole page was replaced, else the sections that were. */
|
|
207
|
+
const swap = (next) => {
|
|
208
|
+
swapHead(next);
|
|
209
|
+
const before = frameOf(doc);
|
|
210
|
+
if (before === null || before !== frameOf(next)) {
|
|
211
|
+
swapPage(next);
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
const incoming = collect(next.body);
|
|
215
|
+
const swapped = [];
|
|
216
|
+
for (const entry of marked) {
|
|
217
|
+
if (entry.parent !== null)
|
|
218
|
+
continue;
|
|
219
|
+
const counterpart = incoming.find((candidate) => candidate.parent === null && candidate.id === entry.id);
|
|
220
|
+
const old = between(entry);
|
|
221
|
+
const fresh = counterpart === undefined ? null : between(counterpart);
|
|
222
|
+
if (old === null || fresh === null || markup(old) === markup(fresh))
|
|
223
|
+
continue;
|
|
224
|
+
for (const node of old)
|
|
225
|
+
node.parentNode?.removeChild(node);
|
|
226
|
+
for (const node of fresh)
|
|
227
|
+
entry.end.parentNode?.insertBefore(doc.importNode(node, true), entry.end);
|
|
228
|
+
swapped.push(entry.id);
|
|
229
|
+
}
|
|
230
|
+
return swapped;
|
|
231
|
+
};
|
|
232
|
+
const render = async (draft, revision) => {
|
|
233
|
+
const ticket = ++latest;
|
|
234
|
+
try {
|
|
235
|
+
const response = await win.fetch(endpoint, {
|
|
236
|
+
method: "POST",
|
|
237
|
+
headers: { "Content-Type": "application/json", Accept: "text/html" },
|
|
238
|
+
body: JSON.stringify({ token, draft }),
|
|
239
|
+
credentials: "same-origin",
|
|
240
|
+
});
|
|
241
|
+
const html = await response.text();
|
|
242
|
+
if (stopped || ticket !== latest)
|
|
243
|
+
return;
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
post({ type: "error", revision, reason: `the preview answered ${response.status}` });
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const swapped = swap(new win.DOMParser().parseFromString(html, "text/html"));
|
|
249
|
+
marked = collect(doc.body);
|
|
250
|
+
if (swapped === null) {
|
|
251
|
+
doc.dispatchEvent(new win.Event(ASTRO_PAGE_LOAD_EVENT));
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
for (const id of swapped)
|
|
255
|
+
doc.dispatchEvent(new win.CustomEvent(PREVIEW_SECTION_LOAD_EVENT, { detail: { id } }));
|
|
256
|
+
}
|
|
257
|
+
post({ type: "rendered", revision, instances: instances() });
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
if (stopped || ticket !== latest)
|
|
261
|
+
return;
|
|
262
|
+
post({ type: "error", revision, reason: error instanceof Error ? error.message : String(error) });
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
const onMessage = (event) => {
|
|
266
|
+
if (event.origin !== options.editorOrigin || event.source !== win.parent)
|
|
267
|
+
return;
|
|
268
|
+
const message = event.data;
|
|
269
|
+
if (typeof message !== "object" || message === null)
|
|
270
|
+
return;
|
|
271
|
+
if (message.source !== "wocart-editor" || message.version !== PREVIEW_PROTOCOL_VERSION)
|
|
272
|
+
return;
|
|
273
|
+
switch (message.type) {
|
|
274
|
+
case "render":
|
|
275
|
+
if (typeof message.draft === "object" && message.draft !== null && !Array.isArray(message.draft)) {
|
|
276
|
+
void render(message.draft, typeof message.revision === "number" ? message.revision : null);
|
|
277
|
+
}
|
|
278
|
+
break;
|
|
279
|
+
case "mode":
|
|
280
|
+
if (message.mode === "edit" || message.mode === "interactive")
|
|
281
|
+
mode = message.mode;
|
|
282
|
+
break;
|
|
283
|
+
case "scroll-to": {
|
|
284
|
+
const entry = marked.find((candidate) => candidate.id === message.id);
|
|
285
|
+
const first = entry === undefined ? undefined : between(entry)?.find((node) => node.nodeType === ELEMENT_NODE);
|
|
286
|
+
first?.scrollIntoView({ block: "start", behavior: "smooth" });
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
case "token":
|
|
290
|
+
if (typeof message.token === "string" && message.token !== "")
|
|
291
|
+
token = message.token;
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
win.addEventListener("message", onMessage);
|
|
296
|
+
win.addEventListener("scroll", reportLayout, { passive: true });
|
|
297
|
+
win.addEventListener("resize", reportLayout);
|
|
298
|
+
doc.addEventListener("click", onClick, true);
|
|
299
|
+
doc.addEventListener("submit", onSubmit, true);
|
|
300
|
+
doc.addEventListener("mouseover", onOver, true);
|
|
301
|
+
doc.documentElement.addEventListener("mouseleave", onLeave);
|
|
302
|
+
post({ type: "ready", instances: instances() });
|
|
303
|
+
return {
|
|
304
|
+
stop() {
|
|
305
|
+
win.removeEventListener("message", onMessage);
|
|
306
|
+
win.removeEventListener("scroll", reportLayout);
|
|
307
|
+
win.removeEventListener("resize", reportLayout);
|
|
308
|
+
doc.removeEventListener("click", onClick, true);
|
|
309
|
+
doc.removeEventListener("submit", onSubmit, true);
|
|
310
|
+
doc.removeEventListener("mouseover", onOver, true);
|
|
311
|
+
doc.documentElement.removeEventListener("mouseleave", onLeave);
|
|
312
|
+
if (frame !== 0)
|
|
313
|
+
win.cancelAnimationFrame(frame);
|
|
314
|
+
stopped = true;
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preview protocol: how a framed storefront page in preview mode and the
|
|
3
|
+
* editor that frames it talk, and how the page marks its sections. Plain data
|
|
4
|
+
* and string helpers — shared by the renderer (`InstanceView`), the page's
|
|
5
|
+
* bridge (`@wocart/theme/preview`) and an editor, so the three cannot drift.
|
|
6
|
+
*
|
|
7
|
+
* Every message is `{ source, version, type, … }`. The page sends
|
|
8
|
+
* `source: "wocart-preview"`; the editor sends `source: "wocart-editor"`. Both
|
|
9
|
+
* sides post with the other's exact origin and check `event.origin` and
|
|
10
|
+
* `event.source` before reading a message.
|
|
11
|
+
*/
|
|
12
|
+
/** Incremented on a change an older counterpart could not read. */
|
|
13
|
+
export declare const PREVIEW_PROTOCOL_VERSION: 1;
|
|
14
|
+
/** Where a theme that supports preview serves it: a POST route of the storefront. */
|
|
15
|
+
export declare const THEME_PREVIEW_PATH = "/wocart-preview";
|
|
16
|
+
/** A section's place on the page, in the frame's viewport coordinates. */
|
|
17
|
+
export interface PreviewRect {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
/** One rendered instance, as the page reports it. `rect` is null when it shows nothing. */
|
|
24
|
+
export interface PreviewInstance {
|
|
25
|
+
id: string;
|
|
26
|
+
type: string;
|
|
27
|
+
/** The instance it is inside, or `null` for a section of the document. */
|
|
28
|
+
parent: string | null;
|
|
29
|
+
hidden: boolean;
|
|
30
|
+
rect: PreviewRect | null;
|
|
31
|
+
}
|
|
32
|
+
/** What the preview page tells the editor. */
|
|
33
|
+
export type PreviewPageMessage = {
|
|
34
|
+
source: "wocart-preview";
|
|
35
|
+
version: typeof PREVIEW_PROTOCOL_VERSION;
|
|
36
|
+
} & ({
|
|
37
|
+
type: "ready";
|
|
38
|
+
instances: PreviewInstance[];
|
|
39
|
+
} | {
|
|
40
|
+
type: "layout";
|
|
41
|
+
instances: PreviewInstance[];
|
|
42
|
+
} | {
|
|
43
|
+
type: "rendered";
|
|
44
|
+
revision: number | null;
|
|
45
|
+
instances: PreviewInstance[];
|
|
46
|
+
} | {
|
|
47
|
+
type: "select";
|
|
48
|
+
id: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: "hover";
|
|
51
|
+
id: string | null;
|
|
52
|
+
} | {
|
|
53
|
+
type: "error";
|
|
54
|
+
revision: number | null;
|
|
55
|
+
reason: string;
|
|
56
|
+
});
|
|
57
|
+
/** The interaction an editor allows: `edit` turns clicks into selections; `interactive` lets the page behave. */
|
|
58
|
+
export type PreviewMode = "edit" | "interactive";
|
|
59
|
+
/**
|
|
60
|
+
* What the editor tells the preview page. `draft` is a `PreviewDraft` of
|
|
61
|
+
* `@wocart/storefront`; `revision`, when given, comes back on the `rendered`
|
|
62
|
+
* or `error` it causes, and a render answered after a newer one is dropped.
|
|
63
|
+
*/
|
|
64
|
+
export type PreviewEditorMessage = {
|
|
65
|
+
source: "wocart-editor";
|
|
66
|
+
version: typeof PREVIEW_PROTOCOL_VERSION;
|
|
67
|
+
} & ({
|
|
68
|
+
type: "render";
|
|
69
|
+
draft: Record<string, unknown>;
|
|
70
|
+
revision?: number;
|
|
71
|
+
} | {
|
|
72
|
+
type: "mode";
|
|
73
|
+
mode: PreviewMode;
|
|
74
|
+
} | {
|
|
75
|
+
type: "scroll-to";
|
|
76
|
+
id: string;
|
|
77
|
+
} | {
|
|
78
|
+
type: "token";
|
|
79
|
+
token: string;
|
|
80
|
+
});
|
|
81
|
+
/** The comment text that opens an instance's nodes. */
|
|
82
|
+
export declare function previewStartMarker(instance: {
|
|
83
|
+
id: string;
|
|
84
|
+
type: string;
|
|
85
|
+
hidden?: boolean;
|
|
86
|
+
}): string;
|
|
87
|
+
/** The comment text that closes them. */
|
|
88
|
+
export declare function previewEndMarker(instance: {
|
|
89
|
+
id: string;
|
|
90
|
+
}): string;
|
|
91
|
+
/** What a marker comment says, or `null` for any other comment. */
|
|
92
|
+
export declare function parsePreviewMarker(text: string): {
|
|
93
|
+
kind: "start";
|
|
94
|
+
id: string;
|
|
95
|
+
type: string;
|
|
96
|
+
hidden: boolean;
|
|
97
|
+
} | {
|
|
98
|
+
kind: "end";
|
|
99
|
+
id: string;
|
|
100
|
+
} | null;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The preview protocol: how a framed storefront page in preview mode and the
|
|
3
|
+
* editor that frames it talk, and how the page marks its sections. Plain data
|
|
4
|
+
* and string helpers — shared by the renderer (`InstanceView`), the page's
|
|
5
|
+
* bridge (`@wocart/theme/preview`) and an editor, so the three cannot drift.
|
|
6
|
+
*
|
|
7
|
+
* Every message is `{ source, version, type, … }`. The page sends
|
|
8
|
+
* `source: "wocart-preview"`; the editor sends `source: "wocart-editor"`. Both
|
|
9
|
+
* sides post with the other's exact origin and check `event.origin` and
|
|
10
|
+
* `event.source` before reading a message.
|
|
11
|
+
*/
|
|
12
|
+
/** Incremented on a change an older counterpart could not read. */
|
|
13
|
+
export const PREVIEW_PROTOCOL_VERSION = 1;
|
|
14
|
+
/** Where a theme that supports preview serves it: a POST route of the storefront. */
|
|
15
|
+
export const THEME_PREVIEW_PATH = "/wocart-preview";
|
|
16
|
+
// ── Section markers ─────────────────────────────────────────────────────────
|
|
17
|
+
// A preview render brackets each instance with two HTML comments, so the page
|
|
18
|
+
// can find every section's nodes — even a section that rendered nothing — and
|
|
19
|
+
// a shopper's page carries none of them.
|
|
20
|
+
const START = "wocart:start";
|
|
21
|
+
const END = "wocart:end";
|
|
22
|
+
/** The comment text that opens an instance's nodes. */
|
|
23
|
+
export function previewStartMarker(instance) {
|
|
24
|
+
return `${START} id=${encodeURIComponent(instance.id)} type=${encodeURIComponent(instance.type)}${instance.hidden === true ? " hidden" : ""}`;
|
|
25
|
+
}
|
|
26
|
+
/** The comment text that closes them. */
|
|
27
|
+
export function previewEndMarker(instance) {
|
|
28
|
+
return `${END} id=${encodeURIComponent(instance.id)}`;
|
|
29
|
+
}
|
|
30
|
+
/** What a marker comment says, or `null` for any other comment. */
|
|
31
|
+
export function parsePreviewMarker(text) {
|
|
32
|
+
const start = /^wocart:start id=([^ ]+) type=([^ ]+)( hidden)?$/.exec(text);
|
|
33
|
+
if (start) {
|
|
34
|
+
try {
|
|
35
|
+
return { kind: "start", id: decodeURIComponent(start[1]), type: decodeURIComponent(start[2]), hidden: start[3] !== undefined };
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const end = /^wocart:end id=([^ ]+)$/.exec(text);
|
|
42
|
+
if (end) {
|
|
43
|
+
try {
|
|
44
|
+
return { kind: "end", id: decodeURIComponent(end[1]) };
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RichTextDocument, RichTextFormat } from "./types.js";
|
|
2
|
+
/** Every format a rich-text field can allow, in the order an editor lists them. */
|
|
3
|
+
export declare const RICH_TEXT_FORMATS: readonly RichTextFormat[];
|
|
4
|
+
/** Whether a stored value is meant as one document (rather than a per-language map). */
|
|
5
|
+
export declare function isRichTextDocumentShape(value: unknown): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* Why `value` is not a valid rich-text document under `formats`, or `null`
|
|
8
|
+
* when it is. Absent `formats` allows every format.
|
|
9
|
+
*/
|
|
10
|
+
export declare function richTextProblem(value: unknown, formats?: readonly RichTextFormat[]): string | null;
|
|
11
|
+
/** How `richTextToHtml` writes links. */
|
|
12
|
+
export interface RichTextHtmlOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Turns a stored link target into the `href` to render — a theme passes its
|
|
15
|
+
* own localizing function so a site path keeps the shopper's language. The
|
|
16
|
+
* result is escaped; a target that fails the link rule never reaches it.
|
|
17
|
+
*/
|
|
18
|
+
href?: (target: string) => string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The document as HTML: `<p>`, `<h2>`–`<h4>`, `<ul>`/`<ol>`, `<strong>`, `<em>`,
|
|
22
|
+
* `<u>`, `<s>`, `<a href>` and `<br>`, every text and attribute escaped. Nodes
|
|
23
|
+
* outside the format and unsafe links are left out rather than trusted, so
|
|
24
|
+
* the output is safe for Astro's `set:html` even for an unchecked value.
|
|
25
|
+
*/
|
|
26
|
+
export declare function richTextToHtml(document: RichTextDocument, options?: RichTextHtmlOptions): string;
|
|
27
|
+
/**
|
|
28
|
+
* The document's text without formatting: blocks and list items on their own
|
|
29
|
+
* lines, line breaks kept — for meta descriptions, `aria-label`s and previews.
|
|
30
|
+
*/
|
|
31
|
+
export declare function richTextToPlainText(document: RichTextDocument): string;
|