@cloudcannon/editable-regions 0.0.16 → 0.0.18
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/helpers/cloudcannon.mjs +7 -22
- package/integrations/astro/astro-integration.mjs +1 -4
- package/integrations/astro/index.mjs +15 -41
- package/integrations/astro/modules/assets.js +1 -4
- package/integrations/astro/modules/content.js +2 -11
- package/integrations/astro/react-renderer.mjs +4 -5
- package/integrations/eleventy/browser/collect-config.mjs +188 -0
- package/integrations/eleventy/browser/index.mjs +9 -0
- package/integrations/eleventy/browser/liquid-builtins.mjs +306 -0
- package/integrations/eleventy/browser/liquid-render.mjs +216 -0
- package/integrations/eleventy/index.cjs +1 -0
- package/integrations/eleventy/index.mjs +582 -0
- package/integrations/liquid/README.md +588 -0
- package/integrations/liquid/errors.mjs +39 -0
- package/integrations/liquid/fs.mjs +25 -74
- package/integrations/liquid/globals.mjs +209 -0
- package/integrations/liquid/include-with-tag.mjs +84 -0
- package/integrations/liquid/index.mjs +163 -170
- package/integrations/liquid/logger.mjs +15 -78
- package/integrations/liquid/page-map.mjs +32 -0
- package/integrations/liquid/shortcodes.mjs +62 -99
- package/integrations/react.mjs +5 -9
- package/nodes/editable-component.ts +6 -6
- package/package.json +19 -8
- package/types/eleventy.d.cts +20 -0
- package/types/eleventy.d.ts +81 -0
- package/types/liquid.d.ts +15 -12
- package/integrations/eleventy.mjs +0 -294
- package/integrations/liquid/11ty-filters.mjs +0 -69
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
// Browser ports of Eleventy's built-in filters and shortcodes. Filters needing
|
|
2
|
+
// build-time internals are warn-once pass-through stubs so templates keep
|
|
3
|
+
// rendering. `registerEleventyBuiltins(engine)` wires everything up.
|
|
4
|
+
|
|
5
|
+
import sindresorhusSlugify from "@sindresorhus/slugify";
|
|
6
|
+
import simovSlugify from "slugify";
|
|
7
|
+
import { warnOnce } from "../../liquid/logger.mjs";
|
|
8
|
+
import { getPageMap, normalizeInputPath } from "../../liquid/page-map.mjs";
|
|
9
|
+
import { createShortcodeTag } from "../../liquid/shortcodes.mjs";
|
|
10
|
+
import {
|
|
11
|
+
createRenderContentFilter,
|
|
12
|
+
createRenderFileShortcode,
|
|
13
|
+
createRenderTemplateTag,
|
|
14
|
+
} from "./liquid-render.mjs";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {any} value
|
|
18
|
+
* @param {string} [prefix]
|
|
19
|
+
*/
|
|
20
|
+
export function logFilter(value, prefix = "") {
|
|
21
|
+
if (prefix) {
|
|
22
|
+
console.log(`[${prefix}]`, value);
|
|
23
|
+
} else {
|
|
24
|
+
console.log(value);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Eleventy's `slug` filter (`Filters/Slug.js`), backed by `simov/slugify`
|
|
31
|
+
* (permissive: keeps `+`, `@`, `.`; substitutes `&`→`and`, `%`→`percent`).
|
|
32
|
+
*
|
|
33
|
+
* @param {unknown} str
|
|
34
|
+
* @param {Record<string, any>} [options]
|
|
35
|
+
*/
|
|
36
|
+
export function slugFilter(str, options = {}) {
|
|
37
|
+
return simovSlugify(`${str}`, { replacement: "-", lower: true, ...options });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Eleventy's `slugify` filter (`Filters/Slugify.js`), backed by
|
|
42
|
+
* `@sindresorhus/slugify` (strict ASCII: non-alphanumerics become separators).
|
|
43
|
+
*
|
|
44
|
+
* @param {unknown} str
|
|
45
|
+
* @param {Record<string, any>} [options]
|
|
46
|
+
*/
|
|
47
|
+
export function slugifyFilter(str, options = {}) {
|
|
48
|
+
return sindresorhusSlugify(`${str}`, { decamelize: false, ...options });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Eleventy's `url` filter (`Filters/Url.js`). Absolute and protocol-relative
|
|
53
|
+
* URLs pass through; root-relative URLs get `pathPrefix` prepended when given.
|
|
54
|
+
* Unlike upstream (which always has a `pathPrefix`), the no-prefix branch
|
|
55
|
+
* returns the input unchanged rather than throwing.
|
|
56
|
+
*
|
|
57
|
+
* @param {string} url
|
|
58
|
+
* @param {string} [pathPrefix]
|
|
59
|
+
*/
|
|
60
|
+
export function urlFilter(url, pathPrefix = "") {
|
|
61
|
+
if (!url) return "";
|
|
62
|
+
|
|
63
|
+
const urlString = String(url);
|
|
64
|
+
if (isAbsoluteUrl(urlString)) return urlString;
|
|
65
|
+
if (urlString.startsWith("//") && urlString !== "//") return urlString;
|
|
66
|
+
if (!pathPrefix) return urlString;
|
|
67
|
+
|
|
68
|
+
const normalizedPrefix = `/${pathPrefix.replace(/^\/+|\/+$/g, "")}`;
|
|
69
|
+
if (urlString.startsWith("/")) return `${normalizedPrefix}${urlString}`;
|
|
70
|
+
|
|
71
|
+
return urlString;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Matches Eleventy's `Util/ValidUrl.js`: parseable by `new URL()` → absolute. */
|
|
75
|
+
function isAbsoluteUrl(/** @type {string} */ url) {
|
|
76
|
+
try {
|
|
77
|
+
new URL(url);
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Coerces an input (Date, ISO string, epoch number) into a Date; `null` for unusable input. */
|
|
85
|
+
function toDate(/** @type {any} */ value) {
|
|
86
|
+
if (value instanceof Date) {
|
|
87
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (value === null || value === undefined || value === "") return null;
|
|
91
|
+
|
|
92
|
+
const d = new Date(value);
|
|
93
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** ISO 8601 / RFC 3339 (e.g. "2026-04-21T00:00:00.000Z"). */
|
|
97
|
+
export function dateToRfc3339(/** @type {Date | string | number} */ date) {
|
|
98
|
+
const d = toDate(date);
|
|
99
|
+
|
|
100
|
+
return d ? d.toISOString() : "";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** RFC 822 / RFC 1123 (e.g. "Tue, 21 Apr 2026 00:00:00 GMT"). */
|
|
104
|
+
export function dateToRfc822(/** @type {Date | string | number} */ date) {
|
|
105
|
+
const d = toDate(date);
|
|
106
|
+
|
|
107
|
+
return d ? d.toUTCString() : "";
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** `YYYY-MM-DD` form, used for `<time datetime>` attributes. */
|
|
111
|
+
export function htmlDateString(/** @type {Date | string | number} */ date) {
|
|
112
|
+
const d = toDate(date);
|
|
113
|
+
|
|
114
|
+
return d ? d.toISOString().slice(0, 10) : "";
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {Array<{date?: any}>} collection
|
|
119
|
+
* @param {Date | string | number} [emptyFallback]
|
|
120
|
+
*/
|
|
121
|
+
export function getNewestCollectionItemDate(collection, emptyFallback) {
|
|
122
|
+
if (!Array.isArray(collection) || collection.length === 0) {
|
|
123
|
+
return toDate(emptyFallback) ?? new Date(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let newest = 0;
|
|
127
|
+
for (const item of collection) {
|
|
128
|
+
const d = toDate(item?.date);
|
|
129
|
+
if (d && d.getTime() > newest) newest = d.getTime();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return newest ? new Date(newest) : (toDate(emptyFallback) ?? new Date(0));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Index of `page` in `collection` by `inputPath`. The editor doesn't model
|
|
137
|
+
* pagination, so `inputPath` alone is unique (upstream also tie-breaks on
|
|
138
|
+
* `outputPath || url`). `await page.inputPath` handles both shapes: the `page`
|
|
139
|
+
* global is a Promise-returning Proxy; a collection item's is already a string.
|
|
140
|
+
*
|
|
141
|
+
* @param {Array<{inputPath?: string}>} collection
|
|
142
|
+
* @param {any} page
|
|
143
|
+
* @returns {Promise<number>}
|
|
144
|
+
*/
|
|
145
|
+
async function indexInCollection(collection, page) {
|
|
146
|
+
if (!Array.isArray(collection)) return -1;
|
|
147
|
+
|
|
148
|
+
if (!page) {
|
|
149
|
+
warnOnce(
|
|
150
|
+
"collection-item-no-page",
|
|
151
|
+
"Eleventy collection-item filter called without a `page` argument. " +
|
|
152
|
+
"In live editing, pass the page/item explicitly (e.g. `collections.posts | getCollectionItem: page`).",
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return -1;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const inputPath = await page.inputPath;
|
|
159
|
+
if (typeof inputPath !== "string" || !inputPath) return -1;
|
|
160
|
+
|
|
161
|
+
return collection.findIndex((item) => item?.inputPath === inputPath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function getCollectionItem(
|
|
165
|
+
/** @type {any[]} */ collection,
|
|
166
|
+
/** @type {any} */ page,
|
|
167
|
+
) {
|
|
168
|
+
const i = await indexInCollection(collection, page);
|
|
169
|
+
|
|
170
|
+
return i >= 0 ? collection[i] : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function getPreviousCollectionItem(
|
|
174
|
+
/** @type {any[]} */ collection,
|
|
175
|
+
/** @type {any} */ page,
|
|
176
|
+
) {
|
|
177
|
+
const i = await indexInCollection(collection, page);
|
|
178
|
+
|
|
179
|
+
return i > 0 ? collection[i - 1] : undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function getNextCollectionItem(
|
|
183
|
+
/** @type {any[]} */ collection,
|
|
184
|
+
/** @type {any} */ page,
|
|
185
|
+
) {
|
|
186
|
+
const i = await indexInCollection(collection, page);
|
|
187
|
+
|
|
188
|
+
return i >= 0 && i < collection.length - 1 ? collection[i + 1] : undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function getCollectionItemIndex(
|
|
192
|
+
/** @type {any[]} */ collection,
|
|
193
|
+
/** @type {any} */ page,
|
|
194
|
+
) {
|
|
195
|
+
return indexInCollection(collection, page);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Pass-through filter that warns once — for Eleventy filters that depend on
|
|
200
|
+
* build-time internals we don't have in the browser.
|
|
201
|
+
*
|
|
202
|
+
* @param {string} filterName
|
|
203
|
+
* @param {string} reason
|
|
204
|
+
*/
|
|
205
|
+
function passThroughStub(filterName, reason) {
|
|
206
|
+
return (/** @type {any} */ value) => {
|
|
207
|
+
warnOnce(
|
|
208
|
+
`filter-stub:${filterName}`,
|
|
209
|
+
`Eleventy filter "${filterName}" is not supported in live editing (${reason}). ` +
|
|
210
|
+
"Returning the input unchanged.",
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
return value;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Browser port of the `inputPathToUrl` plugin filter, resolving against the
|
|
219
|
+
* build-time page map. Misses (e.g. a file not in the last build) warn-once
|
|
220
|
+
* and pass through rather than throwing.
|
|
221
|
+
*
|
|
222
|
+
* @param {unknown} inputPath
|
|
223
|
+
*/
|
|
224
|
+
export function inputPathToUrlFilter(inputPath) {
|
|
225
|
+
if (typeof inputPath !== "string" || !inputPath) {
|
|
226
|
+
return typeof inputPath === "string" ? inputPath : "";
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const entry = getPageMap()[normalizeInputPath(inputPath)];
|
|
230
|
+
if (entry?.url) return entry.url;
|
|
231
|
+
|
|
232
|
+
warnOnce(
|
|
233
|
+
`input-path-to-url-miss:${inputPath}`,
|
|
234
|
+
`inputPathToUrl: no build-time URL recorded for "${inputPath}". ` +
|
|
235
|
+
"This usually means the file wasn't in the last build. Returning the " +
|
|
236
|
+
"input unchanged.",
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
return inputPath;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** @type {Record<string, any>} */
|
|
243
|
+
export const eleventyFilters = {
|
|
244
|
+
slug: slugFilter,
|
|
245
|
+
slugify: slugifyFilter,
|
|
246
|
+
log: logFilter,
|
|
247
|
+
url: urlFilter,
|
|
248
|
+
dateToRfc3339,
|
|
249
|
+
dateToRfc822,
|
|
250
|
+
htmlDateString,
|
|
251
|
+
getNewestCollectionItemDate,
|
|
252
|
+
getCollectionItem,
|
|
253
|
+
getPreviousCollectionItem,
|
|
254
|
+
getNextCollectionItem,
|
|
255
|
+
getCollectionItemIndex,
|
|
256
|
+
inputPathToUrl: inputPathToUrlFilter,
|
|
257
|
+
htmlBaseUrl: passThroughStub(
|
|
258
|
+
"htmlBaseUrl",
|
|
259
|
+
"it requires Eleventy's pathPrefix/HTML base config",
|
|
260
|
+
),
|
|
261
|
+
serverlessUrl: passThroughStub(
|
|
262
|
+
"serverlessUrl",
|
|
263
|
+
"serverless routing is a build-time concept",
|
|
264
|
+
),
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Builtin names the config auto-mirror skips, so a same-named config helper
|
|
269
|
+
* doesn't clobber our port. Derived from the implementations so they can't
|
|
270
|
+
* drift; `renderContent`/`renderFile` are the RenderPlugin shims.
|
|
271
|
+
*
|
|
272
|
+
* @type {string[]}
|
|
273
|
+
*/
|
|
274
|
+
export const builtinFilterNames = [
|
|
275
|
+
...Object.keys(eleventyFilters),
|
|
276
|
+
"renderContent",
|
|
277
|
+
];
|
|
278
|
+
|
|
279
|
+
/** @type {string[]} */
|
|
280
|
+
export const builtinShortcodeNames = ["renderFile"];
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Wires `eleventyFilters` plus the RenderPlugin shims onto the shared engine.
|
|
284
|
+
*
|
|
285
|
+
* @param {import("liquidjs").Liquid} liquidEngine
|
|
286
|
+
*/
|
|
287
|
+
export function registerEleventyBuiltins(liquidEngine) {
|
|
288
|
+
for (const [name, fn] of Object.entries(eleventyFilters)) {
|
|
289
|
+
liquidEngine.registerFilter(name, fn);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
liquidEngine.registerTag(
|
|
293
|
+
"renderTemplate",
|
|
294
|
+
createRenderTemplateTag(liquidEngine),
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
liquidEngine.registerTag(
|
|
298
|
+
"renderFile",
|
|
299
|
+
createShortcodeTag("renderFile", createRenderFileShortcode(liquidEngine)),
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
liquidEngine.registerFilter(
|
|
303
|
+
"renderContent",
|
|
304
|
+
createRenderContentFilter(liquidEngine),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser shims for Eleventy's RenderPlugin
|
|
3
|
+
* (https://v3.11ty.dev/docs/plugins/render/): `renderTemplate` (paired tag),
|
|
4
|
+
* `renderContent` (filter), `renderFile` (shortcode). Each compiles a body as
|
|
5
|
+
* `templateLang` and renders it with `data`. Only LiquidJS runs in the browser,
|
|
6
|
+
* so: "liquid"/unspecified → real render; "html" → passthrough; else →
|
|
7
|
+
* warn-once and return the body unchanged.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
apiLoadedPromise,
|
|
12
|
+
CloudCannon,
|
|
13
|
+
} from "../../../helpers/cloudcannon.mjs";
|
|
14
|
+
import { warnOnce } from "../../liquid/logger.mjs";
|
|
15
|
+
import { evaluateArgs, parseArgs } from "../../liquid/shortcodes.mjs";
|
|
16
|
+
|
|
17
|
+
const supportedEngines = new Set(["liquid", "html"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tag factory for `{% renderTemplate ... %}…{% endrenderTemplate %}`. Captures
|
|
21
|
+
* the body as raw source (matching upstream — compiled in the requested engine,
|
|
22
|
+
* not pre-rendered) and renders it against `data`.
|
|
23
|
+
*
|
|
24
|
+
* @param {any} _liquidEngine - Unused; reached via `this.liquid`
|
|
25
|
+
* @returns {any}
|
|
26
|
+
*/
|
|
27
|
+
export function createRenderTemplateTag(_liquidEngine) {
|
|
28
|
+
return {
|
|
29
|
+
parse(/** @type {any} */ tagToken, /** @type {any[]} */ remainTokens) {
|
|
30
|
+
this.name = tagToken.name;
|
|
31
|
+
this.argTokens = parseArgs(
|
|
32
|
+
tagToken.args,
|
|
33
|
+
this.liquid.options.operatorsTrie,
|
|
34
|
+
);
|
|
35
|
+
this.bodyTokens = [];
|
|
36
|
+
|
|
37
|
+
const endTagName = `end${this.name}`;
|
|
38
|
+
while (remainTokens.length) {
|
|
39
|
+
const token = remainTokens.shift();
|
|
40
|
+
if (token.name === endTagName) return;
|
|
41
|
+
|
|
42
|
+
this.bodyTokens.push(token);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
throw new Error(`tag ${this.name} not closed`);
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async render(/** @type {any} */ context) {
|
|
49
|
+
const args = await evaluateArgs(this.argTokens, context);
|
|
50
|
+
const { templateLang, data } = normalizeRenderArgs([args[0], args[1]]);
|
|
51
|
+
const body = this.bodyTokens
|
|
52
|
+
.map((/** @type {any} */ t) => t.getText())
|
|
53
|
+
.join("");
|
|
54
|
+
|
|
55
|
+
if (templateLang && !supportedEngines.has(templateLang)) {
|
|
56
|
+
warnOnce(
|
|
57
|
+
`render-template:${templateLang}`,
|
|
58
|
+
unsupportedEngineMessage(templateLang),
|
|
59
|
+
);
|
|
60
|
+
return body;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (templateLang === "html") return body;
|
|
64
|
+
return await this.liquid.parseAndRender(body, data);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Builds the `renderContent` filter, capturing the shared engine.
|
|
71
|
+
*
|
|
72
|
+
* @param {any} liquidEngine
|
|
73
|
+
*/
|
|
74
|
+
export function createRenderContentFilter(liquidEngine) {
|
|
75
|
+
return async function renderContent(
|
|
76
|
+
/** @type {any} */ content,
|
|
77
|
+
/** @type {any} */ templateLang,
|
|
78
|
+
/** @type {any} */ data,
|
|
79
|
+
) {
|
|
80
|
+
const normalized = normalizeRenderArgs([templateLang, data]);
|
|
81
|
+
const body = content == null ? "" : String(content);
|
|
82
|
+
|
|
83
|
+
if (
|
|
84
|
+
normalized.templateLang &&
|
|
85
|
+
!supportedEngines.has(normalized.templateLang)
|
|
86
|
+
) {
|
|
87
|
+
warnOnce(
|
|
88
|
+
`render-content:${normalized.templateLang}`,
|
|
89
|
+
unsupportedEngineMessage(normalized.templateLang),
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return body;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (normalized.templateLang === "html") return body;
|
|
96
|
+
|
|
97
|
+
return await liquidEngine.parseAndRender(body, normalized.data);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds the `renderFile` shortcode. Fetches `inputPath` via the CloudCannon
|
|
103
|
+
* API and renders its body with `data`. Engine comes from `templateLang`, else
|
|
104
|
+
* inferred from the file extension.
|
|
105
|
+
*
|
|
106
|
+
* @param {any} liquidEngine
|
|
107
|
+
*/
|
|
108
|
+
export function createRenderFileShortcode(liquidEngine) {
|
|
109
|
+
return async function renderFile(
|
|
110
|
+
/** @type {any} */ inputPath,
|
|
111
|
+
/** @type {any} */ data,
|
|
112
|
+
/** @type {any} */ templateLang,
|
|
113
|
+
) {
|
|
114
|
+
if (typeof inputPath !== "string" || !inputPath) {
|
|
115
|
+
warnOnce(
|
|
116
|
+
"render-file:no-path",
|
|
117
|
+
"renderFile: missing or non-string path argument. Returning empty.",
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
return "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Normalise Eleventy-style paths (`./foo`, `/foo`) to the
|
|
124
|
+
// project-relative shape the CC API expects.
|
|
125
|
+
const normalizedPath = inputPath.replace(/^\.\/+/, "").replace(/^\/+/, "");
|
|
126
|
+
|
|
127
|
+
await apiLoadedPromise;
|
|
128
|
+
|
|
129
|
+
const file = CloudCannon?.file?.(normalizedPath);
|
|
130
|
+
if (!file) {
|
|
131
|
+
warnOnce(
|
|
132
|
+
`render-file-missing:${inputPath}`,
|
|
133
|
+
`renderFile: CloudCannon API not available; cannot load "${inputPath}".`,
|
|
134
|
+
);
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Mirror 11ty's data cascade: the file's own front matter is the base,
|
|
139
|
+
// the caller's `data` arg overrides on top. `content.get()` strips it.
|
|
140
|
+
let body;
|
|
141
|
+
let frontMatter;
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
[body, frontMatter] = await Promise.all([
|
|
145
|
+
file.content.get(),
|
|
146
|
+
file.data.get(),
|
|
147
|
+
]);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
warnOnce(
|
|
150
|
+
`render-file-missing:${inputPath}`,
|
|
151
|
+
`renderFile: failed to load "${inputPath}" via the CloudCannon API ` +
|
|
152
|
+
`(${err instanceof Error ? err.message : String(err)}).`,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
return "";
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const engine =
|
|
159
|
+
(typeof templateLang === "string" && templateLang) ||
|
|
160
|
+
inferEngineFromPath(inputPath);
|
|
161
|
+
|
|
162
|
+
if (engine && !supportedEngines.has(engine)) {
|
|
163
|
+
warnOnce(`render-file:${engine}`, unsupportedEngineMessage(engine));
|
|
164
|
+
|
|
165
|
+
return body;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (engine === "html") return body;
|
|
169
|
+
|
|
170
|
+
const mergedData = { ...(frontMatter ?? {}), ...(data ?? {}) };
|
|
171
|
+
|
|
172
|
+
return await liquidEngine.parseAndRender(body, mergedData);
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Normalises the `(templateLang, data)` pair, supporting the `(lang, data)`
|
|
178
|
+
* and lang-omitted `(data)` overloads.
|
|
179
|
+
*
|
|
180
|
+
* @param {[any, any]} args
|
|
181
|
+
* @returns {{templateLang: string | undefined, data: any}}
|
|
182
|
+
*/
|
|
183
|
+
function normalizeRenderArgs([templateLang, data]) {
|
|
184
|
+
if (templateLang && typeof templateLang !== "string") {
|
|
185
|
+
data = templateLang;
|
|
186
|
+
templateLang = undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { templateLang, data: data ?? {} };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Guesses the engine from the extension so the warn-once message can name it;
|
|
194
|
+
* only "liquid"/"html" actually render, the rest fall through to passthrough.
|
|
195
|
+
*
|
|
196
|
+
* @param {string} inputPath
|
|
197
|
+
*/
|
|
198
|
+
function inferEngineFromPath(inputPath) {
|
|
199
|
+
const dot = inputPath.lastIndexOf(".");
|
|
200
|
+
if (dot < 0) return undefined;
|
|
201
|
+
|
|
202
|
+
const ext = inputPath.slice(dot + 1).toLowerCase();
|
|
203
|
+
if (ext === "liquid") return "liquid";
|
|
204
|
+
if (ext === "html" || ext === "htm") return "html";
|
|
205
|
+
if (ext === "md") return "md";
|
|
206
|
+
if (ext === "njk") return "njk";
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function unsupportedEngineMessage(/** @type {string} */ engineName) {
|
|
211
|
+
return (
|
|
212
|
+
`Eleventy RenderPlugin: engine "${engineName}" is not supported in ` +
|
|
213
|
+
`live editing (only "liquid" and "html" run in the browser). ` +
|
|
214
|
+
"Returning the body unchanged."
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./index.mjs").default;
|