@lupinum/ginko-content 0.3.3 → 0.3.5

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.
@@ -84,20 +84,20 @@ function fieldFromSchema(key, schema, localized) {
84
84
  const required = !["ZodOptional", "ZodNullable", "ZodDefault"].includes(getSchemaTypeName(schema) ?? "");
85
85
  if (metadata) return fieldFromMetadata(key, metadata, schema, unwrapped, localized, required);
86
86
  const reference = getReferenceDescriptor(unwrapped);
87
- if (reference) return field({ key, type: "relation", required, default: defaultFromSchema(schema), relation: { collection: reference.collection ?? "", multiple: false } });
87
+ if (reference) return field({ key, type: "relation", required, default: defaultFromSchema(schema, key), relation: { collection: reference.collection ?? "", multiple: false } });
88
88
  const typeName = getSchemaTypeName(unwrapped);
89
- if (typeName === "ZodObject") return field({ key, type: "object", required, localized, default: defaultFromSchema(schema), fields: nestedFields(unwrapped), validation: validationFromSchema(schema) });
89
+ if (typeName === "ZodObject") return field({ key, type: "object", required, localized, default: defaultFromSchema(schema, key), fields: nestedFields(unwrapped), validation: validationFromSchema(schema) });
90
90
  if (typeName === "ZodArray") {
91
91
  const element = getSchemaDef(unwrapped)?.element;
92
92
  const relation = getReferenceDescriptor(unwrapSchema(element));
93
- if (relation) return field({ key, type: "relations", required, default: defaultFromSchema(schema), relation: { collection: relation.collection ?? "", multiple: true } });
93
+ if (relation) return field({ key, type: "relations", required, default: defaultFromSchema(schema, key), relation: { collection: relation.collection ?? "", multiple: true } });
94
94
  const children = getSchemaTypeName(unwrapSchema(element)) === "ZodObject" ? nestedFields(element) : null;
95
- return field({ key, type: children ? "array" : "json", required, localized, default: defaultFromSchema(schema), fields: children, validation: validationFromSchema(schema) });
95
+ return field({ key, type: children ? "array" : "json", required, localized, default: defaultFromSchema(schema, key), fields: children, validation: validationFromSchema(schema) });
96
96
  }
97
- if (typeName === "ZodNumber") return field({ key, type: "number", required, default: defaultFromSchema(schema), validation: validationFromSchema(schema) });
98
- if (typeName === "ZodBoolean") return field({ key, type: "toggle", required, default: defaultFromSchema(schema), validation: validationFromSchema(schema) });
99
- if (typeName === "ZodDate") return field({ key, type: "date", required, default: defaultFromSchema(schema), validation: validationFromSchema(schema) });
100
- return field({ key, type: typeName === "ZodString" ? "text" : "json", required, localized, default: defaultFromSchema(schema), validation: validationFromSchema(schema) });
97
+ if (typeName === "ZodNumber") return field({ key, type: "number", required, default: defaultFromSchema(schema, key), validation: validationFromSchema(schema) });
98
+ if (typeName === "ZodBoolean") return field({ key, type: "toggle", required, default: defaultFromSchema(schema, key), validation: validationFromSchema(schema) });
99
+ if (typeName === "ZodDate") return field({ key, type: "date", required, default: defaultFromSchema(schema, key), validation: validationFromSchema(schema) });
100
+ return field({ key, type: typeName === "ZodString" ? "text" : "json", required, localized, default: defaultFromSchema(schema, key), validation: validationFromSchema(schema) });
101
101
  }
102
102
  function fieldFromMetadata(key, metadata, sourceSchema, schema, localized, required) {
103
103
  const type = metadata.type === "boolean" ? "toggle" : metadata.type === "asset" ? "file" : metadata.type;
@@ -107,7 +107,7 @@ function fieldFromMetadata(key, metadata, sourceSchema, schema, localized, requi
107
107
  type,
108
108
  required: metadata.required ?? required,
109
109
  localized: metadata.localized ?? localized,
110
- default: defaultFromSchema(sourceSchema),
110
+ default: defaultFromSchema(sourceSchema, key),
111
111
  options: metadata.options ?? null,
112
112
  relation: metadata.relation ? { collection: metadata.relation.collectionId, multiple: metadata.relation.multiple ?? type === "relations" } : null,
113
113
  media: metadata.image || metadata.asset ? { mediaTypes: portableMediaTypes(metadata.image?.accept ?? metadata.asset?.accept), aspectRatio: metadata.image?.aspectRatio ?? null } : null,
@@ -132,7 +132,7 @@ function mergeField(existing, key, override, localized) {
132
132
  localized: override.localized ?? base.localized,
133
133
  searchable: override.searchable ?? base.searchable,
134
134
  sortable: override.sortable ?? base.sortable,
135
- default: Object.prototype.hasOwnProperty.call(override, "defaultValue") ? { present: true, value: override.defaultValue } : base.default,
135
+ default: Object.prototype.hasOwnProperty.call(override, "defaultValue") ? { present: true, value: jsonDefault(override.defaultValue, key) } : base.default,
136
136
  options: override.options ?? base.options,
137
137
  relation: override.relation ? { collection: override.relation.collectionId, multiple: override.relation.multiple ?? type === "relations" } : base.relation,
138
138
  fields: override.fields ? configuredFields(override.fields, false) : base.fields,
@@ -143,17 +143,45 @@ function mergeField(existing, key, override, localized) {
143
143
  language: override.language ?? base.language
144
144
  });
145
145
  }
146
- function defaultFromSchema(schema) {
146
+ function defaultFromSchema(schema, key) {
147
147
  let current = schema;
148
148
  while (current) {
149
149
  const typeName = getSchemaTypeName(current);
150
150
  const definition = getSchemaDef(current);
151
- if (typeName === "ZodDefault") return { present: true, value: definition?.defaultValue };
151
+ if (typeName === "ZodDefault") return { present: true, value: jsonDefault(definition?.defaultValue, key) };
152
152
  if (!["ZodOptional", "ZodNullable"].includes(typeName ?? "")) break;
153
153
  current = definition?.innerType;
154
154
  }
155
155
  return { present: false };
156
156
  }
157
+ function jsonDefault(value, key, ancestors = /* @__PURE__ */ new Set()) {
158
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
159
+ if (typeof value === "number") {
160
+ if (!Number.isFinite(value)) throw new TypeError(`Field "${key}" has a non-finite default value.`);
161
+ return value;
162
+ }
163
+ if (value instanceof Date) {
164
+ if (Number.isNaN(value.getTime())) throw new TypeError(`Field "${key}" has an invalid date default.`);
165
+ return value.toISOString();
166
+ }
167
+ if (typeof value !== "object" || value === null) throw new TypeError(`Field "${key}" has a non-JSON-serializable default value.`);
168
+ if (ancestors.has(value)) throw new TypeError(`Field "${key}" has a cyclic default value.`);
169
+ ancestors.add(value);
170
+ try {
171
+ if (Array.isArray(value)) return value.map((entry, index) => jsonDefault(entry, `${key}[${index}]`, ancestors));
172
+ const prototype = Object.getPrototypeOf(value);
173
+ if (prototype !== Object.prototype && prototype !== null) throw new TypeError(`Field "${key}" has a non-JSON-serializable default value.`);
174
+ if (Object.getOwnPropertySymbols(value).length > 0) throw new TypeError(`Field "${key}" has a non-JSON-serializable default value.`);
175
+ const result = {};
176
+ for (const [property, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(value))) {
177
+ if (!descriptor.enumerable || !("value" in descriptor)) throw new TypeError(`Field "${key}" has a non-JSON-serializable default value.`);
178
+ result[property] = jsonDefault(descriptor.value, `${key}.${property}`, ancestors);
179
+ }
180
+ return result;
181
+ } finally {
182
+ ancestors.delete(value);
183
+ }
184
+ }
157
185
  function validationFromSchema(schema, semanticType) {
158
186
  const typeName = getSchemaTypeName(schema);
159
187
  const definition = getSchemaDef(schema);
@@ -170,16 +198,16 @@ function validationFromSchema(schema, semanticType) {
170
198
  const format = ["email", "url", "date", "datetime", "time"].includes(String(value.format)) ? value.format : null;
171
199
  return {
172
200
  kind: "string",
173
- minLength: typeof value.minLength === "number" ? value.minLength : null,
174
- maxLength: typeof value.maxLength === "number" ? value.maxLength : null,
201
+ minLength: finiteNumber(value.minLength),
202
+ maxLength: finiteNumber(value.maxLength),
175
203
  format
176
204
  };
177
205
  }
178
206
  if (typeName === "ZodNumber") {
179
207
  return {
180
208
  kind: "number",
181
- min: typeof value.minValue === "number" ? value.minValue : null,
182
- max: typeof value.maxValue === "number" ? value.maxValue : null,
209
+ min: finiteNumber(value.minValue),
210
+ max: finiteNumber(value.maxValue),
183
211
  integer: value.isInt === true
184
212
  };
185
213
  }
@@ -210,12 +238,17 @@ function arrayLimits(checks) {
210
238
  if (Array.isArray(checks)) {
211
239
  for (const check of checks) {
212
240
  const definition = check._zod?.def ?? check.def;
213
- if (definition?.check === "min_length" && typeof definition.minimum === "number") minItems = definition.minimum;
214
- if (definition?.check === "max_length" && typeof definition.maximum === "number") maxItems = definition.maximum;
241
+ const minimum = finiteNumber(definition?.minimum);
242
+ const maximum = finiteNumber(definition?.maximum);
243
+ if (definition?.check === "min_length" && minimum !== null) minItems = minimum;
244
+ if (definition?.check === "max_length" && maximum !== null) maxItems = maximum;
215
245
  }
216
246
  }
217
247
  return { minItems, maxItems };
218
248
  }
249
+ function finiteNumber(value) {
250
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
251
+ }
219
252
  function configuredFields(input, localized) {
220
253
  const entries = Array.isArray(input) ? input.map((value, index) => [String(index), value]) : Object.entries(input);
221
254
  return entries.flatMap(([key, value]) => value.type === "divider" || value.type === "section" ? [] : [mergeField(void 0, key, value, localized)]);
package/dist/module.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "configKey": "content",
5
5
  "compatibility": {
6
6
  "nuxt": ">=4.4.7 <5"
package/dist/module.mjs CHANGED
@@ -30,7 +30,7 @@ import { createRouterMatcher } from 'vue-router';
30
30
  import { globby } from 'globby';
31
31
 
32
32
  const name = "@lupinum/ginko-content";
33
- const version = "0.3.3";
33
+ const version = "0.3.5";
34
34
  const peerDependencies = {
35
35
  nuxt: ">=4.4.7 <5"};
36
36
 
@@ -4,7 +4,7 @@ import { computed, onServerPrefetch, shallowRef, useAttrs, watch } from "vue";
4
4
  import { useRuntimeConfig, useState } from "#imports";
5
5
  import MarkdownRenderer from "./internal/MarkdownRenderer.js";
6
6
  import { useUnwrap } from "../composables/useUnwrap";
7
- import { resolveMarkdownPlugins, resolveMarkdownRendererComponents } from "../../markdown/plugins";
7
+ import { resolveMarkdownPlugins, resolveMarkdownRendererComponents, resolveMarkdownRendererFallbackComponents } from "../../markdown/plugins";
8
8
  import { toMarkdownRoot } from "../../../core/markdown/tree";
9
9
  import { parseComark } from "../../../core/markdown/parse-comark";
10
10
  import { loadContentComponentEntries } from "../../../integrations/vue/content-components";
@@ -44,6 +44,7 @@ const initialState = useState(stateKey, () => ({
44
44
  tree: null
45
45
  }));
46
46
  const tree = shallowRef(initialState.value.tree);
47
+ const fallbackComponents = resolveMarkdownRendererFallbackComponents();
47
48
  const resolvedComponents = computed(() => ({
48
49
  ...Object.fromEntries(loadContentComponentEntries({
49
50
  type: "root",
@@ -92,6 +93,7 @@ watch(
92
93
  :tag="tag"
93
94
  :prose="prose"
94
95
  :components="resolvedComponents"
96
+ :fallback-components="fallbackComponents"
95
97
  v-bind="rendererAttrs"
96
98
  />
97
99
  </template>
@@ -7,7 +7,7 @@ import MarkdownRenderer from "./MarkdownRenderer.js";
7
7
  import { useLocalePath } from "../../composables/content-i18n";
8
8
  import { resolveMarkdownRenderRefs, rewriteMarkdownRefLinks } from "../../../../core/references/resolve";
9
9
  import { loadContentComponentEntries } from "../../../../integrations/vue/content-components";
10
- import { resolveMarkdownRendererComponents } from "../../../markdown/plugins";
10
+ import { resolveMarkdownRendererComponents, resolveMarkdownRendererFallbackComponents } from "../../../markdown/plugins";
11
11
  import { isMarkdownRoot } from "../../../../core/markdown/tree";
12
12
  const props = defineProps({
13
13
  value: {
@@ -79,6 +79,7 @@ const renderedBody = computed(() => {
79
79
  }
80
80
  return props.unwrap ? unwrapRoot(body.value, props.unwrap) : body.value;
81
81
  });
82
+ const fallbackComponents = resolveMarkdownRendererFallbackComponents();
82
83
  const resolvedComponents = computed(() => {
83
84
  if (!renderedBody.value) {
84
85
  return props.components;
@@ -108,6 +109,7 @@ const rendererAttrs = computed(() => {
108
109
  :default-locale="defaultLocale"
109
110
  :locales="locales"
110
111
  :components="resolvedComponents"
112
+ :fallback-components="fallbackComponents"
111
113
  :render-policy="renderPolicy"
112
114
  :data-content-id="debug ? value.id : void 0"
113
115
  v-bind="rendererAttrs"
@@ -11,6 +11,10 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
11
11
  type: PropType<Record<string, any>>;
12
12
  default: () => {};
13
13
  };
14
+ fallbackComponents: {
15
+ type: PropType<Record<string, any>>;
16
+ default: () => {};
17
+ };
14
18
  prose: {
15
19
  type: PropType<boolean | undefined>;
16
20
  default: undefined;
@@ -56,6 +60,10 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
56
60
  type: PropType<Record<string, any>>;
57
61
  default: () => {};
58
62
  };
63
+ fallbackComponents: {
64
+ type: PropType<Record<string, any>>;
65
+ default: () => {};
66
+ };
59
67
  prose: {
60
68
  type: PropType<boolean | undefined>;
61
69
  default: undefined;
@@ -97,6 +105,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
97
105
  components: Record<string, any>;
98
106
  tag: string;
99
107
  class: string | unknown[] | Record<string, any>;
108
+ fallbackComponents: Record<string, any>;
100
109
  prose: boolean | undefined;
101
110
  dataContentId: string;
102
111
  renderPolicy: PortableComponentPolicyV1;
@@ -23,7 +23,7 @@ export function localizeMarkdownNodeProps(props, locale, defaultLocale, locales
23
23
  localizeLinkProps(localizedProps, locale, defaultLocale, locales);
24
24
  return localizedProps;
25
25
  }
26
- function resolveVueComponent(name, components, registry, prose, seen = /* @__PURE__ */ new Set()) {
26
+ function resolveVueComponent(name, components, registry, fallbacks, prose, seen = /* @__PURE__ */ new Set()) {
27
27
  if (seen.has(name)) {
28
28
  return null;
29
29
  }
@@ -40,7 +40,7 @@ function resolveVueComponent(name, components, registry, prose, seen = /* @__PUR
40
40
  const explicit = components[candidate];
41
41
  if (explicit) {
42
42
  if (typeof explicit === "string" && explicit !== candidate && !htmlTags.includes(explicit)) {
43
- return resolveVueComponent(explicit, components, registry, prose, seen) || explicit;
43
+ return resolveVueComponent(explicit, components, registry, fallbacks, prose, seen) || explicit;
44
44
  }
45
45
  return explicit;
46
46
  }
@@ -51,6 +51,12 @@ function resolveVueComponent(name, components, registry, prose, seen = /* @__PUR
51
51
  return registered;
52
52
  }
53
53
  }
54
+ for (const candidate of candidates) {
55
+ const fallback = fallbacks[candidate];
56
+ if (fallback) {
57
+ return fallback;
58
+ }
59
+ }
54
60
  return null;
55
61
  }
56
62
  function renderNode(node, options, key, parent) {
@@ -72,8 +78,8 @@ function renderNode(node, options, key, parent) {
72
78
  if (parent?.tag === "pre") {
73
79
  component = tag;
74
80
  } else {
75
- const resolvedAs = typeof nodeProps.as === "string" ? resolveVueComponent(nodeProps.as, options.components, options.registry, options.prose) : null;
76
- const resolvedComponent = resolvedAs || resolveVueComponent(tag, options.components, options.registry, options.prose);
81
+ const resolvedAs = typeof nodeProps.as === "string" ? resolveVueComponent(nodeProps.as, options.components, options.registry, options.fallbacks, options.prose) : null;
82
+ const resolvedComponent = resolvedAs || resolveVueComponent(tag, options.components, options.registry, options.fallbacks, options.prose);
77
83
  if (resolvedComponent) {
78
84
  component = resolvedComponent;
79
85
  } else if (htmlTags.includes(tag)) {
@@ -143,6 +149,10 @@ export default defineComponent({
143
149
  type: Object,
144
150
  default: () => ({})
145
151
  },
152
+ fallbackComponents: {
153
+ type: Object,
154
+ default: () => ({})
155
+ },
146
156
  prose: {
147
157
  type: Boolean,
148
158
  default: void 0
@@ -184,6 +194,7 @@ export default defineComponent({
184
194
  const children = (props.tree.children || []).map((node, index) => renderNode(node, {
185
195
  components: props.components,
186
196
  registry,
197
+ fallbacks: props.fallbackComponents,
187
198
  prose: props.prose,
188
199
  locale: props.locale,
189
200
  defaultLocale: props.defaultLocale,
@@ -1,3 +1,8 @@
1
1
  import type { ResolvedMarkdownPlugin } from '../../types/content';
2
2
  export { resolveMarkdownPlugins } from '../../parsers/markdown-plugins';
3
+ /**
4
+ * Builtin prose components resolve after the app's component registry, so an
5
+ * app-registered ProseImg (or a tags remap) wins over the bundled default.
6
+ */
7
+ export declare function resolveMarkdownRendererFallbackComponents(): Record<string, unknown>;
3
8
  export declare function resolveMarkdownRendererComponents(plugins: ResolvedMarkdownPlugin[]): Record<string, unknown>;
@@ -23,10 +23,11 @@ const builtinMarkdownPlugins = {
23
23
  const builtinMarkdownComponents = {
24
24
  ProseImg: defineAsyncComponent(async () => await import("../app/components/Prose/ProseImg.vue"))
25
25
  };
26
+ export function resolveMarkdownRendererFallbackComponents() {
27
+ return { ...builtinMarkdownComponents };
28
+ }
26
29
  export function resolveMarkdownRendererComponents(plugins) {
27
- const components = {
28
- ...builtinMarkdownComponents
29
- };
30
+ const components = {};
30
31
  for (const plugin of plugins) {
31
32
  const builtin = builtinMarkdownPlugins[plugin.name];
32
33
  if (builtin?.componentName && builtin.component) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "framework": "vue",
3
3
  "name": "@lupinum/ginko-content",
4
- "version": "0.3.3",
4
+ "version": "0.3.5",
5
5
  "contributions": {
6
6
  "html": {
7
7
  "description-markup": "markdown",
@@ -202,6 +202,14 @@
202
202
  },
203
203
  "default": "() => ({})"
204
204
  },
205
+ {
206
+ "name": "fallbackComponents",
207
+ "value": {
208
+ "kind": "expression",
209
+ "type": "Record<string, any>"
210
+ },
211
+ "default": "() => ({})"
212
+ },
205
213
  {
206
214
  "name": "prose",
207
215
  "value": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/ginko-content",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Collection-first content engine for Nuxt",
5
5
  "homepage": "https://github.com/lupinum-dev/ginko-content",
6
6
  "bugs": {