@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.
@@ -1,92 +1,29 @@
1
- /**
2
- * Shortcode-to-LiquidJS-Tag wrapper utilities.
3
- * Converts Eleventy-style shortcode functions into LiquidJS custom tags.
4
- *
5
- * Eleventy shortcodes: simple functions that return HTML
6
- * LiquidJS tags: objects with parse() and render() methods
7
- */
1
+ // Wraps Eleventy-style shortcode functions (return a string) as LiquidJS
2
+ // custom tags (objects with parse/render).
8
3
 
9
4
  import { evalToken, Tokenizer, toPromise } from "liquidjs";
10
5
  import { group, groupEnd, log } from "./logger.mjs";
11
6
 
12
7
  /**
13
- * Parses comma-separated arguments from a tag's args string.
14
- * Handles quoted strings and variable references.
15
- *
16
- * @param {string} argsString - Raw arguments string from tagToken.args
17
- * @param {any} operatorsTrie - Liquid options operatorsTrie
18
- * @returns {any[]} Array of parsed tokens
19
- */
20
- function parseArgs(argsString, operatorsTrie) {
21
- if (!argsString || !argsString.trim()) {
22
- return [];
23
- }
24
-
25
- const tokenizer = new Tokenizer(argsString, operatorsTrie);
26
- const tokens = [];
27
-
28
- while (true) {
29
- tokenizer.skipBlank();
30
- const token = tokenizer.readValue();
31
- if (!token) break;
32
- tokens.push(token);
33
-
34
- tokenizer.skipBlank();
35
- if (tokenizer.peek() === ",") {
36
- tokenizer.advance();
37
- } else {
38
- break;
39
- }
40
- }
41
-
42
- return tokens;
43
- }
44
-
45
- /**
46
- * Evaluates parsed tokens against the render context.
47
- *
48
- * @param {any[]} tokens - Array of parsed tokens
49
- * @param {any} context - LiquidJS render context
50
- * @returns {Promise<any[]>} Array of evaluated values
51
- */
52
- async function evaluateArgs(tokens, context) {
53
- const values = [];
54
- for (const token of tokens) {
55
- const value = await toPromise(evalToken(token, context));
56
- values.push(value);
57
- }
58
- return values;
59
- }
60
-
61
- /**
62
- * Creates a LiquidJS tag implementation for a regular (non-paired) shortcode.
63
- *
64
8
  * Usage: {% shortcodeName arg1, arg2, "literal" %}
65
9
  *
66
- * @param {any} shortcodeFn - The shortcode function (arg1, arg2, ...) => string
67
- * @param {string} shortcodeName - The shortcode name for logging
68
- * @returns {import('liquidjs/dist/template/tag-options-adapter').TagImplOptions} LiquidJS tag implementation
10
+ * @param {string} shortcodeName
11
+ * @param {any} shortcodeFn
12
+ * @returns {import('liquidjs/dist/template/tag-options-adapter').TagImplOptions}
69
13
  */
70
- export function createShortcodeTag(shortcodeFn, shortcodeName) {
14
+ export function createShortcodeTag(shortcodeName, shortcodeFn) {
71
15
  /** @type {any} */
72
16
  const tag = {
73
- /**
74
- * @param {any} tagToken - The tag token from LiquidJS parser
75
- */
76
- parse(tagToken) {
17
+ parse(/** @type {any} */ tagToken) {
77
18
  this.argTokens = parseArgs(
78
19
  tagToken.args,
79
20
  this.liquid.options.operatorsTrie,
80
21
  );
81
22
  },
82
23
 
83
- /**
84
- * @param {any} context - The LiquidJS render context
85
- */
86
- async render(context) {
87
- log(`Executing shortcode "${shortcodeName}"`);
24
+ async render(/** @type {any} */ context) {
88
25
  const args = await evaluateArgs(this.argTokens, context);
89
- log("Shortcode args:", args);
26
+ log(`Shortcode "${shortcodeName}" args:`, args);
90
27
  const result = await shortcodeFn(...args);
91
28
  log("Shortcode returned:", result?.substring?.(0, 100) || result);
92
29
  return result ?? "";
@@ -96,64 +33,44 @@ export function createShortcodeTag(shortcodeFn, shortcodeName) {
96
33
  }
97
34
 
98
35
  /**
99
- * Creates a LiquidJS tag implementation for a paired shortcode.
100
- *
101
36
  * Usage: {% shortcodeName arg1 %}content{% endshortcodeName %}
102
37
  *
103
- * @param {string} tagName - The shortcode/tag name (needed to find end tag)
104
- * @param {any} shortcodeFn - The shortcode function (content, arg1, ...) => string
105
- * @returns {import('liquidjs/dist/template/tag-options-adapter').TagImplOptions} LiquidJS tag implementation
38
+ * @param {string} tagName
39
+ * @param {any} shortcodeFn
40
+ * @returns {import('liquidjs/dist/template/tag-options-adapter').TagImplOptions}
106
41
  */
107
42
  export function createPairedShortcodeTag(tagName, shortcodeFn) {
108
43
  const endTagName = `end${tagName}`;
109
44
 
110
45
  /** @type {any} */
111
46
  const tag = {
112
- /**
113
- * @param {any} tagToken - The tag token from LiquidJS parser
114
- * @param {any} remainTokens - Remaining tokens to parse
115
- */
116
- parse(tagToken, remainTokens) {
47
+ parse(/** @type {any} */ tagToken, /** @type {any} */ remainTokens) {
117
48
  this.argTokens = parseArgs(
118
49
  tagToken.args,
119
50
  this.liquid.options.operatorsTrie,
120
51
  );
121
52
  this.templates = [];
122
53
 
123
- // Consume tokens until we find the end tag
124
54
  while (remainTokens.length) {
125
55
  const token = remainTokens.shift();
126
-
127
- // Check if this is our end tag
128
- if (token.name === endTagName) {
129
- break;
130
- }
131
-
132
- // Parse this token into a template and add to our templates
56
+ if (token.name === endTagName) break;
133
57
  const template = this.liquid.parser.parseToken(token, remainTokens);
134
58
  this.templates.push(template);
135
59
  }
136
60
  },
137
61
 
138
- /**
139
- * @param {any} context - The LiquidJS render context
140
- */
141
- async render(context) {
62
+ async render(/** @type {any} */ context) {
142
63
  group(`Paired shortcode "${tagName}"`);
143
- log("Inner templates to render:", this.templates.length);
144
64
 
145
- // Render the content between the tags
146
- // NOTE: renderTemplates returns a generator, must use toPromise() to resolve it
65
+ // renderTemplates returns a generator toPromise resolves it.
147
66
  const content = await toPromise(
148
67
  this.liquid.renderer.renderTemplates(this.templates, context),
149
68
  );
150
69
  log("Content resolved:", content);
151
70
 
152
- // Evaluate arguments
153
71
  const args = await evaluateArgs(this.argTokens, context);
154
72
  log("Args:", args);
155
73
 
156
- // Call shortcode with content as first argument, then other args
157
74
  const result = await shortcodeFn(content, ...args);
158
75
  log("Final HTML:", result?.substring?.(0, 100) || result);
159
76
  groupEnd();
@@ -163,3 +80,49 @@ export function createPairedShortcodeTag(tagName, shortcodeFn) {
163
80
  };
164
81
  return tag;
165
82
  }
83
+
84
+ /**
85
+ * Parses comma-separated tag arguments (quoted strings and variable refs).
86
+ *
87
+ * @param {string} argsString
88
+ * @param {any} operatorsTrie
89
+ * @returns {any[]}
90
+ */
91
+ export function parseArgs(argsString, operatorsTrie) {
92
+ if (!argsString?.trim()) {
93
+ return [];
94
+ }
95
+
96
+ const tokenizer = new Tokenizer(argsString, operatorsTrie);
97
+ const tokens = [];
98
+
99
+ while (true) {
100
+ tokenizer.skipBlank();
101
+ const token = tokenizer.readValue();
102
+ if (!token) break;
103
+ tokens.push(token);
104
+
105
+ tokenizer.skipBlank();
106
+ if (tokenizer.peek() === ",") {
107
+ tokenizer.advance();
108
+ } else {
109
+ break;
110
+ }
111
+ }
112
+
113
+ return tokens;
114
+ }
115
+
116
+ /**
117
+ * @param {any[]} tokens
118
+ * @param {any} context
119
+ * @returns {Promise<any[]>}
120
+ */
121
+ export async function evaluateArgs(tokens, context) {
122
+ const values = [];
123
+ for (const token of tokens) {
124
+ const value = await toPromise(evalToken(token, context));
125
+ values.push(value);
126
+ }
127
+ return values;
128
+ }
@@ -4,19 +4,15 @@ import { createRoot } from "react-dom/client";
4
4
  import { addEditableComponentRenderer } from "../helpers/cloudcannon.mjs";
5
5
 
6
6
  /**
7
- * Registers a React component with the CloudCannon component system.
8
- * Creates a wrapper that renders the React component to an HTMLElement.
7
+ * Registers a React component, wrapping it to render to an HTMLElement.
9
8
  *
10
- * @param {string} key - Unique identifier for the component
11
- * @param {any} component - The React component function to register
12
- * @returns {void}
9
+ * @param {string} key
10
+ * @param {any} component
13
11
  */
14
12
  export const registerReactComponent = (key, component) => {
15
13
  /**
16
- * Wrapper function that renders the React component to an HTMLElement.
17
- *
18
- * @param {any} props - Props to pass to the React component
19
- * @returns {HTMLElement} The rendered component as an HTMLElement
14
+ * @param {any} props
15
+ * @returns {HTMLElement}
20
16
  */
21
17
  const wrappedComponent = (props) => {
22
18
  const reactNode = createElement(component, props, null);
@@ -215,6 +215,12 @@ export default class EditableComponent extends Editable {
215
215
  continue;
216
216
  }
217
217
 
218
+ // Both children are the same subclass (node/element) but are of different types (e.g. text -> comment)
219
+ if (renderChild.nodeName !== targetChild.nodeName) {
220
+ targetChild.replaceWith(renderChild);
221
+ continue;
222
+ }
223
+
218
224
  // Both existing and rendered children are nodes (i.e. some text)
219
225
  if (
220
226
  !(renderChild instanceof Element) &&
@@ -225,12 +231,6 @@ export default class EditableComponent extends Editable {
225
231
  continue;
226
232
  }
227
233
 
228
- // Both children are elements but are different types
229
- if (renderChild.nodeName !== targetChild.nodeName) {
230
- targetChild.replaceWith(renderChild);
231
- continue;
232
- }
233
-
234
234
  // Both existing and rendered children are the same kind of element, and neither is editable
235
235
  if (!isEditableElement(renderChild) && !isEditableElement(targetChild)) {
236
236
  // Update the existing element to match the rendered element and recurse their subtrees
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudcannon/editable-regions",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "type": "module",
5
5
  "description": "Visual Editing for the CloudCannon CMS.",
6
6
  "keywords": [
@@ -58,21 +58,32 @@
58
58
  "default": "./integrations/astro/svelte-renderer.mjs"
59
59
  },
60
60
  "./liquid": "./integrations/liquid/index.mjs",
61
- "./eleventy": "./integrations/eleventy.mjs",
61
+ "./eleventy": {
62
+ "require": {
63
+ "types": "./types/eleventy.d.cts",
64
+ "default": "./integrations/eleventy/index.cjs"
65
+ },
66
+ "import": {
67
+ "types": "./types/eleventy.d.ts",
68
+ "default": "./integrations/eleventy/index.mjs"
69
+ }
70
+ },
71
+ "./eleventy/browser": "./integrations/eleventy/browser/index.mjs",
62
72
  "./internal/components": "./components/index.js",
63
73
  "./internal/styles": "./styles/index.js"
64
74
  },
65
75
  "devDependencies": {
66
- "@biomejs/biome": "2.5.0",
67
- "@cloudcannon/visual-editor-api": "0.0.18",
76
+ "@biomejs/biome": "2.5.3",
77
+ "@cloudcannon/visual-editor-api": "0.0.19",
68
78
  "@sindresorhus/slugify": "3.0.0",
69
79
  "@types/js-beautify": "1.14.3",
70
- "@types/node": "25.9.4",
80
+ "@types/node": "26.1.1",
71
81
  "@types/react": "19.2.17",
72
82
  "@types/react-dom": "19.2.3",
73
- "astro": "6.4.8",
74
- "js-beautify": "1.15.4",
75
- "liquidjs": "10.27.0",
83
+ "astro": "7.0.7",
84
+ "js-beautify": "2.0.3",
85
+ "liquidjs": "10.27.2",
86
+ "slugify": "1.6.9",
76
87
  "typescript": "6.0.3"
77
88
  },
78
89
  "dependencies": {
@@ -0,0 +1,20 @@
1
+ /**
2
+ * CommonJS declaration shape for `@cloudcannon/editable-regions/eleventy`.
3
+ * Mirrors the ESM `.d.ts` but uses `export = ` so `require()` resolves to
4
+ * the function directly. Shared interfaces are re-imported from the ESM
5
+ * declarations to keep them in one place.
6
+ */
7
+ import type {
8
+ LiquidOptions,
9
+ NormalizedPluginOptions,
10
+ PluginOptions,
11
+ } from "./eleventy";
12
+
13
+ declare function editableRegions(
14
+ eleventyConfig: any,
15
+ pluginOptions: PluginOptions,
16
+ ): void;
17
+ declare namespace editableRegions {
18
+ export { LiquidOptions, NormalizedPluginOptions, PluginOptions };
19
+ }
20
+ export = editableRegions;
@@ -0,0 +1,81 @@
1
+ export interface LiquidOptions {
2
+ /** Directories to walk for component templates. Defaults to `[directories.includes, directories.input]`. */
3
+ componentDirs?: string[];
4
+ /** Template file extensions to bundle. Defaults to `[".liquid", ".html"]`. */
5
+ extensions?: string[];
6
+ /** Directory names to skip when walking. Defaults to `[directories.output, "node_modules"]`. */
7
+ ignoreDirectories?: string[];
8
+ /** Map of component name → module path. Wins over the auto-discovered components. */
9
+ components?: Record<string, string>;
10
+ /**
11
+ * Path to the Eleventy config file, used to auto-mirror its helpers into
12
+ * the browser bundle. Resolved relative to the project root. Defaults to
13
+ * the first of 11ty's standard names that exists (`.eleventy.js`,
14
+ * `eleventy.config.{js,mjs,cjs}`). Set this only if you run Eleventy with a
15
+ * non-default `--config` path.
16
+ */
17
+ configPath?: string;
18
+ /**
19
+ * Extra bare module specifiers to stub out of the browser bundle, on top of
20
+ * the 11ty toolchain and Node built-ins (always stubbed). Use this when the
21
+ * config imports a native/Node-only package (e.g. `sharp`) that no
22
+ * browser-bound helper actually calls at render time but that would
23
+ * otherwise break bundling.
24
+ */
25
+ browserStub?: string[];
26
+ /**
27
+ * Browser-side filter overrides: filter name → module path. The config's
28
+ * filters are auto-mirrored into the browser by bundling the real config,
29
+ * so closures and imports survive. Use an override only when a filter
30
+ * genuinely can't run in the browser (it calls a Node API at render time);
31
+ * the override replaces it and its name is excluded from the mirror.
32
+ */
33
+ filters?: Record<string, string>;
34
+ /** Browser-side shortcode overrides. Same auto-mirror + override model as `filters`. */
35
+ shortcodes?: Record<string, string>;
36
+ /** Browser-side paired-shortcode overrides. Same auto-mirror + override model as `filters`. */
37
+ pairedShortcodes?: Record<string, string>;
38
+ /**
39
+ * Browser-side custom Liquid tag overrides: tag name → module path
40
+ * (default-exporting a `(engine) => { parse, render }` factory). Tags are
41
+ * auto-mirrored from the config like filters/shortcodes; supply an override
42
+ * here only for a tag that can't run in the browser as written.
43
+ */
44
+ tags?: Record<string, string>;
45
+ }
46
+
47
+ export interface PluginOptions {
48
+ /** Output path for the generated bundle. Defaults to `register-components.js` inside Eleventy's `dir.output`. */
49
+ output?: string;
50
+ /** Enable verbose browser logging. */
51
+ verbose?: boolean;
52
+ /**
53
+ * Liquid is the plugin's default language and is enabled implicitly.
54
+ * Pass `false` to disable, `true` for defaults, or an options object
55
+ * for customisation.
56
+ */
57
+ liquid?: LiquidOptions | boolean;
58
+ /**
59
+ * Extra globals to expose to editor-rendered templates, mirroring whatever
60
+ * global data your build already provides (via `_data/` or
61
+ * `addGlobalData`). Embedded into the bundle at build time, so values must
62
+ * be JSON-serialisable. To surface env vars, pass them in explicitly, e.g.
63
+ * `globals: { env: { API_BASE: process.env.API_BASE } }`, and register the
64
+ * same data server-side so the editor and build agree. Don't include secrets.
65
+ */
66
+ globals?: Record<string, unknown>;
67
+ }
68
+
69
+ /**
70
+ * Internal shape after `normalizePluginOptions`: each supported language is
71
+ * resolved to either an options object (enabled) or `false` (disabled). Same
72
+ * shape as `PluginOptions` aside from that resolution.
73
+ */
74
+ export type NormalizedPluginOptions = Omit<PluginOptions, "liquid"> & {
75
+ liquid: LiquidOptions | false;
76
+ };
77
+
78
+ export default function (
79
+ eleventyConfig: any,
80
+ pluginOptions: PluginOptions,
81
+ ): void;
package/types/liquid.d.ts CHANGED
@@ -1,33 +1,29 @@
1
1
  declare module "@cloudcannon/editable-regions/liquid" {
2
- import type { Liquid } from "liquidjs";
3
-
4
- interface LiquidConfig {
5
- componentDirs?: string[];
6
- }
2
+ import type { Liquid, LiquidOptions } from "liquidjs";
7
3
 
8
4
  export function setVerbose(value: boolean): void;
9
5
  export function log(...args: any[]): void;
10
6
  export function group(label?: string): void;
11
7
  export function groupEnd(): void;
12
8
 
13
- export function configureLiquid(options: LiquidConfig): void;
14
- export function getLiquidEngine(options?: Record<string, any>): Liquid;
9
+ export function createSharedLiquidEngine(options?: LiquidOptions): Liquid;
15
10
  export function registerLiquidComponent(key: string, contents: string): void;
11
+ export function initComponentProxy(): void;
16
12
 
17
- export function createBindIncludeTag(liquidEngine: Liquid): {
13
+ export function createIncludeWithTag(liquidEngine: Liquid): {
18
14
  parse(tagToken: any): void;
19
15
  render(context: any): Promise<string>;
20
16
  };
21
17
 
22
- export function registerCustomFilter(
18
+ export function registerFilter(
23
19
  name: string,
24
20
  fn: (...args: any[]) => any,
25
21
  ): void;
26
- export function registerCustomShortcode(
22
+ export function registerShortcode(
27
23
  name: string,
28
24
  fn: (...args: any[]) => any,
29
25
  ): void;
30
- export function registerCustomPairedShortcode(
26
+ export function registerPairedShortcode(
31
27
  name: string,
32
28
  fn: (...args: any[]) => any,
33
29
  ): void;
@@ -35,6 +31,13 @@ declare module "@cloudcannon/editable-regions/liquid" {
35
31
  name: string,
36
32
  factory: (liquidEngine: Liquid) => any,
37
33
  ): void;
34
+ export function registerProcessEnv(env: Record<string, string>): void;
35
+ export function registerEleventyData(data: {
36
+ version: string;
37
+ generator: string;
38
+ env: { runMode: string; source: string };
39
+ directories: Record<string, string>;
40
+ }): void;
38
41
  }
39
42
 
40
43
  /** Window globals used by the liquid integration */
@@ -46,7 +49,7 @@ declare global {
46
49
  (props: Record<string, any>) => Promise<HTMLElement>
47
50
  >;
48
51
  /** Liquid template files keyed by path */
49
- cc_files?: Record<string, string>;
52
+ cc_liquid_files?: Record<string, string>;
50
53
  }
51
54
  }
52
55