@taprootio/site-authoring 0.8.2 → 0.8.3
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/package.json +1 -1
- package/src/constants.js +1 -1
- package/src/content/tiptap-prosemirror.ts +121 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taprootio/site-authoring",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
4
4
|
"description": "Taproot site authoring CLI: sign in once in the browser, then pull, validate, push, preview, approve, and deploy a site from the terminal",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "ISC",
|
package/src/constants.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export const CLI_NAME = "@taprootio/site-authoring";
|
|
2
|
-
export const CLI_VERSION = "0.8.
|
|
2
|
+
export const CLI_VERSION = "0.8.3";
|
|
3
3
|
export const CLI_BINARY_NAME = "taproot-site";
|
|
4
4
|
export const RESULT_SCHEMA_VERSION = 1;
|
|
5
5
|
export const CONFIG_FILE_NAME = "taproot-site.json";
|
|
@@ -68,6 +68,16 @@ export interface RenderProseMirrorOptions {
|
|
|
68
68
|
/** The page shell emits this default once so rich-text formats inherit it. */
|
|
69
69
|
maxHeightVh?: TiptapImageMaxHeightVh | number | null;
|
|
70
70
|
};
|
|
71
|
+
/** Generator-only render hooks for structured nodes that need page context. */
|
|
72
|
+
nodeRenderers?: {
|
|
73
|
+
componentBlock?: (node: ProseMirrorNode) => string | undefined;
|
|
74
|
+
taprootImage?: (node: ProseMirrorNode) => string | undefined;
|
|
75
|
+
rawHtml?: (html: string, node: ProseMirrorNode) => string | undefined;
|
|
76
|
+
};
|
|
77
|
+
/** Records visible document-order content after each node is rendered. */
|
|
78
|
+
onRenderedNode?: (node: ProseMirrorNode, html: string) => void;
|
|
79
|
+
/** Claims the page's one LCP slot before rendering a section photo. */
|
|
80
|
+
claimSectionBackgroundImage?: () => boolean;
|
|
71
81
|
}
|
|
72
82
|
|
|
73
83
|
export function createEmptyProseMirrorDocument(): ProseMirrorDocument {
|
|
@@ -97,11 +107,62 @@ export function renderRichTextBodyToHtml(
|
|
|
97
107
|
body: unknown,
|
|
98
108
|
options: RenderProseMirrorOptions = {},
|
|
99
109
|
): string {
|
|
100
|
-
if (typeof body === "string") return body;
|
|
101
110
|
if (!isProseMirrorDocument(body)) return "";
|
|
102
111
|
return renderProseMirrorDocumentToHtml(body, options);
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Extracts the reader-visible text from a ProseMirror document for metadata
|
|
116
|
+
* that only accepts plain text, such as Schema.org recipe instructions.
|
|
117
|
+
*/
|
|
118
|
+
export function extractRichTextBodyText(body: unknown): string {
|
|
119
|
+
if (!isProseMirrorDocument(body)) return "";
|
|
120
|
+
const parts: string[] = [];
|
|
121
|
+
collectText(body, parts);
|
|
122
|
+
return parts.join("").replace(/\s+/gu, " ").trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function collectText(node: ProseMirrorNode, parts: string[]): void {
|
|
126
|
+
if (node.type === "text") {
|
|
127
|
+
if (node.text) parts.push(node.text);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// rawHtml is an intentional, already-sanitized node in the JSON document
|
|
132
|
+
// contract. It is not the retired outer HTML-body transport. Preserve its
|
|
133
|
+
// reader-visible text when a consumer (for example Recipe JSON-LD) needs a
|
|
134
|
+
// plain-text representation of the document.
|
|
135
|
+
if (node.type === "rawHtml") {
|
|
136
|
+
const html = stringAttr(node.attrs?.html);
|
|
137
|
+
if (html) parts.push(plainTextFromRawHtml(html));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
for (const child of node.content ?? []) {
|
|
142
|
+
collectText(child, parts);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (node.type === "hardBreak" || isTextBlock(node.type)) {
|
|
146
|
+
parts.push(" ");
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isTextBlock(type: string): boolean {
|
|
151
|
+
return type === "paragraph"
|
|
152
|
+
|| type === "heading"
|
|
153
|
+
|| type === "listItem"
|
|
154
|
+
|| type === "blockquote"
|
|
155
|
+
|| type === "codeBlock"
|
|
156
|
+
|| type === "tableCell"
|
|
157
|
+
|| type === "tableHeader";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function plainTextFromRawHtml(html: string): string {
|
|
161
|
+
return html
|
|
162
|
+
.replace(/<(?:br\s*\/?|\/(?:p|div|li|h[1-6]|blockquote|pre|tr|table|section|article))\s*[^>]*>/giu, " ")
|
|
163
|
+
.replace(/<[^>]*>/gu, "");
|
|
164
|
+
}
|
|
165
|
+
|
|
105
166
|
type RenderPlacement = "root" | "section" | "nested";
|
|
106
167
|
|
|
107
168
|
function renderNodes(
|
|
@@ -109,7 +170,11 @@ function renderNodes(
|
|
|
109
170
|
options: RenderProseMirrorOptions,
|
|
110
171
|
placement: RenderPlacement,
|
|
111
172
|
): string {
|
|
112
|
-
return nodes.map((node) =>
|
|
173
|
+
return nodes.map((node) => {
|
|
174
|
+
const html = renderNode(node, options, placement);
|
|
175
|
+
options.onRenderedNode?.(node, html);
|
|
176
|
+
return html;
|
|
177
|
+
}).join("");
|
|
113
178
|
}
|
|
114
179
|
|
|
115
180
|
function renderFreeFormRootNodes(
|
|
@@ -193,11 +258,11 @@ function renderNode(
|
|
|
193
258
|
case "table":
|
|
194
259
|
return placement === "root" || placement === "section" ? renderTable(node, options) : "";
|
|
195
260
|
case "taprootImage":
|
|
196
|
-
return renderTaprootImage(node, options);
|
|
261
|
+
return options.nodeRenderers?.taprootImage?.(node) ?? renderTaprootImage(node, options);
|
|
197
262
|
case "inlineFacts":
|
|
198
263
|
return renderInlineFacts(node, options, placement);
|
|
199
264
|
case "componentBlock":
|
|
200
|
-
return renderComponentBlock(node);
|
|
265
|
+
return options.nodeRenderers?.componentBlock?.(node) ?? renderComponentBlock(node);
|
|
201
266
|
case "integrationPlacement":
|
|
202
267
|
return renderIntegrationPlacement(node, options);
|
|
203
268
|
case "section":
|
|
@@ -208,8 +273,10 @@ function renderNode(
|
|
|
208
273
|
options,
|
|
209
274
|
placement === "root" ? "section" : "nested",
|
|
210
275
|
);
|
|
211
|
-
case "rawHtml":
|
|
212
|
-
|
|
276
|
+
case "rawHtml": {
|
|
277
|
+
const html = stringAttr(node.attrs?.html) ?? "";
|
|
278
|
+
return options.nodeRenderers?.rawHtml?.(html, node) ?? html;
|
|
279
|
+
}
|
|
213
280
|
default:
|
|
214
281
|
return renderNodes(node.content ?? [], options, "nested");
|
|
215
282
|
}
|
|
@@ -650,6 +717,9 @@ function renderSection(
|
|
|
650
717
|
if (decorationStyle) sectionStyleParts.push(...decorationStyle);
|
|
651
718
|
}
|
|
652
719
|
const background = kind === "explicit" ? normalizeSectionBackground(attrs.background) : undefined;
|
|
720
|
+
const backgroundPicture = background
|
|
721
|
+
? renderSectionBackgroundPicture(background, options.claimSectionBackgroundImage?.() === true)
|
|
722
|
+
: "";
|
|
653
723
|
if (background) {
|
|
654
724
|
sectionStyleParts.push(
|
|
655
725
|
`--esp-section-background: color-mix(in oklab, var(--esp-color-background) ${background.scrimOpacityPercent}%, transparent)`,
|
|
@@ -695,7 +765,7 @@ function renderSection(
|
|
|
695
765
|
style:
|
|
696
766
|
`--taproot-section-focus: ${background.focus}; --taproot-section-portrait-focus: ${background.portraitFocus}`,
|
|
697
767
|
},
|
|
698
|
-
`${
|
|
768
|
+
`${backgroundPicture}${section}`,
|
|
699
769
|
);
|
|
700
770
|
}
|
|
701
771
|
|
|
@@ -845,7 +915,7 @@ function normalizeSectionFocus(
|
|
|
845
915
|
return `${x * 100}% ${y * 100}%`;
|
|
846
916
|
}
|
|
847
917
|
|
|
848
|
-
function renderSectionBackgroundPicture(background: NormalizedSectionBackground): string {
|
|
918
|
+
function renderSectionBackgroundPicture(background: NormalizedSectionBackground, hero: boolean): string {
|
|
849
919
|
const portraitSource = background.portraitImage
|
|
850
920
|
? renderVoidElement("source", {
|
|
851
921
|
media: "(max-width: 48rem)",
|
|
@@ -862,7 +932,8 @@ function renderSectionBackgroundPicture(background: NormalizedSectionBackground)
|
|
|
862
932
|
height: background.image.height,
|
|
863
933
|
alt: "",
|
|
864
934
|
decoding: "async",
|
|
865
|
-
loading: "lazy",
|
|
935
|
+
loading: hero ? undefined : "lazy",
|
|
936
|
+
fetchpriority: hero ? "high" : undefined,
|
|
866
937
|
});
|
|
867
938
|
return renderElement(
|
|
868
939
|
"picture",
|
|
@@ -1032,19 +1103,50 @@ function renderComponentBlock(node: ProseMirrorNode): string {
|
|
|
1032
1103
|
);
|
|
1033
1104
|
}
|
|
1034
1105
|
|
|
1035
|
-
|
|
1106
|
+
export interface ResolvedTiptapImage {
|
|
1107
|
+
src: string;
|
|
1108
|
+
urls: Array<{ minWidth: number; url: string; type?: string }>;
|
|
1109
|
+
width: number;
|
|
1110
|
+
height: number;
|
|
1111
|
+
alt: string;
|
|
1112
|
+
imageId: string;
|
|
1113
|
+
preserveStoredSrc: boolean;
|
|
1114
|
+
extraAttributes: Record<string, string | number | undefined>;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
export function resolveTiptapImage(
|
|
1118
|
+
node: ProseMirrorNode,
|
|
1119
|
+
imageDefaults: RenderProseMirrorOptions["imageDefaults"],
|
|
1120
|
+
): ResolvedTiptapImage {
|
|
1036
1121
|
const attrs = node.attrs ?? {};
|
|
1037
|
-
const
|
|
1122
|
+
const crop = jsonAttr(attrs.crop);
|
|
1123
|
+
const resolved = resolveImagePresentation(attrs, imageDefaults);
|
|
1124
|
+
return {
|
|
1125
|
+
src: stringAttr(attrs.src) ?? "",
|
|
1126
|
+
urls: validResponsiveImageUrls(attrs.urls),
|
|
1127
|
+
width: numberAttr(attrs.width) ?? 0,
|
|
1128
|
+
height: numberAttr(attrs.height) ?? 0,
|
|
1129
|
+
alt: stringAttr(attrs.alt) ?? "",
|
|
1130
|
+
imageId: stringAttr(attrs.imageId) ?? "",
|
|
1131
|
+
preserveStoredSrc: Boolean(crop && crop !== "null"),
|
|
1132
|
+
extraAttributes: {
|
|
1133
|
+
...(crop ? { "data-crop": crop } : {}),
|
|
1134
|
+
...resolved,
|
|
1135
|
+
},
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function renderTaprootImage(node: ProseMirrorNode, options: RenderProseMirrorOptions): string {
|
|
1140
|
+
const image = resolveTiptapImage(node, options.imageDefaults);
|
|
1038
1141
|
const imageAttrs: Record<string, string | number | boolean | null | undefined> = {
|
|
1039
|
-
"low-res":
|
|
1040
|
-
"original-width":
|
|
1041
|
-
"original-height":
|
|
1042
|
-
"data-image-id":
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
...resolved,
|
|
1142
|
+
"low-res": image.src,
|
|
1143
|
+
"original-width": image.width,
|
|
1144
|
+
"original-height": image.height,
|
|
1145
|
+
"data-image-id": image.imageId,
|
|
1146
|
+
caption: image.alt,
|
|
1147
|
+
...image.extraAttributes,
|
|
1046
1148
|
};
|
|
1047
|
-
const optionsHtml =
|
|
1149
|
+
const optionsHtml = image.urls
|
|
1048
1150
|
.map((url) =>
|
|
1049
1151
|
renderVoidElement("esp-image-option", {
|
|
1050
1152
|
width: url.minWidth,
|