@rigel-cms/rigel 1.2.2 → 1.4.0-rc.1

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.
Files changed (44) hide show
  1. package/docs/content-locales.md +8 -0
  2. package/package.json +1 -1
  3. package/src/editor/App.svelte +52 -28
  4. package/src/editor/i18n/de/fields.json +3 -0
  5. package/src/editor/i18n/de/media.json +9 -1
  6. package/src/editor/i18n/de/shell.json +4 -0
  7. package/src/editor/i18n/en/fields.json +3 -0
  8. package/src/editor/i18n/en/media.json +9 -1
  9. package/src/editor/i18n/en/shell.json +4 -0
  10. package/src/editor/i18n/es/fields.json +3 -0
  11. package/src/editor/i18n/es/media.json +9 -1
  12. package/src/editor/i18n/es/shell.json +4 -0
  13. package/src/editor/i18n/fr/fields.json +3 -0
  14. package/src/editor/i18n/fr/media.json +9 -1
  15. package/src/editor/i18n/fr/shell.json +4 -0
  16. package/src/editor/i18n/it/fields.json +3 -0
  17. package/src/editor/i18n/it/media.json +9 -1
  18. package/src/editor/i18n/it/shell.json +4 -0
  19. package/src/editor/i18n/ja/fields.json +3 -0
  20. package/src/editor/i18n/ja/media.json +9 -1
  21. package/src/editor/i18n/ja/shell.json +4 -0
  22. package/src/editor/i18n/nl/fields.json +3 -0
  23. package/src/editor/i18n/nl/media.json +9 -1
  24. package/src/editor/i18n/nl/shell.json +4 -0
  25. package/src/editor/i18n/ru/fields.json +3 -0
  26. package/src/editor/i18n/ru/media.json +9 -1
  27. package/src/editor/i18n/ru/shell.json +4 -0
  28. package/src/editor/i18n/zh/fields.json +3 -0
  29. package/src/editor/i18n/zh/media.json +9 -1
  30. package/src/editor/i18n/zh/shell.json +4 -0
  31. package/src/editor/lib/ActionDialog.svelte +23 -3
  32. package/src/editor/lib/ImagePicker.svelte +27 -19
  33. package/src/editor/lib/MarkdownEditor.svelte +7 -5
  34. package/src/editor/lib/SchemaField.svelte +2 -2
  35. package/src/editor/lib/SchemaPanel.svelte +76 -4
  36. package/src/editor/lib/VisualEditor.svelte +38 -3
  37. package/src/editor/lib/icons.ts +4 -0
  38. package/src/editor/styles.css +15 -3
  39. package/src/manifest/content-locales.ts +1 -1
  40. package/src/manifest/navigation.ts +4 -3
  41. package/src/manifest/pages.ts +17 -0
  42. package/src/media-types.ts +17 -0
  43. package/src/schema/types.ts +3 -1
  44. package/src/schema/validate.ts +8 -3
@@ -32,3 +32,20 @@ export function safeMediaName(name: string) {
32
32
  export function mediaAccept(imagesOnly = false) {
33
33
  return Object.keys(MEDIA_TYPES).filter(ext => !imagesOnly || MEDIA_TYPES[ext].category === 'Images').map(ext => '.' + ext).join(',');
34
34
  }
35
+
36
+ export function matchesMediaAccept(name: string, accept?: string): boolean {
37
+ const metadata = mediaMetadata(name);
38
+ if (!metadata) return false;
39
+ return !accept || accept.toLowerCase().split(',').some(token => {
40
+ const spec = token.trim();
41
+ return spec === '.' + metadata.extension || spec === metadata.type || (spec.endsWith('/*') && metadata.type.startsWith(spec.slice(0, -1)));
42
+ });
43
+ }
44
+
45
+ export function safeFileUrl(value: string): boolean {
46
+ if (!value.startsWith('/') || value.startsWith('//') || /[\s\u0000-\u001f\u007f\\?#]/.test(value)) return false;
47
+ try {
48
+ const parts = value.slice(1).split('/').map(part => decodeURIComponent(part));
49
+ return parts.every(part => !!part && part !== '.' && part !== '..' && !/[\u0000-\u001f\u007f/\\?#]/.test(part)) && !!mediaMetadata(parts.at(-1)!);
50
+ } catch { return false; }
51
+ }
@@ -1,4 +1,4 @@
1
- export type ScalarFieldType = "string" | "text" | "markdown" | "boolean" | "number" | "date" | "image" | "url" | "color" | "code" | "password";
1
+ export type ScalarFieldType = "string" | "text" | "markdown" | "boolean" | "number" | "date" | "image" | "file" | "url" | "color" | "code" | "password";
2
2
 
3
3
  export type ArrayItem =
4
4
  | { type: "object"; name?: string; title?: string; fields?: FieldDef[] }
@@ -11,6 +11,7 @@ export interface FieldDef {
11
11
  labelKey?: string;
12
12
  type: ScalarFieldType | "object" | "array" | "reference";
13
13
  placeholder?: string;
14
+ accept?: string;
14
15
  required?: boolean;
15
16
  initialValue?: unknown;
16
17
  fields?: FieldDef[];
@@ -41,6 +42,7 @@ export interface DocumentDef {
41
42
  folder: string;
42
43
  fields: FieldDef[];
43
44
  columns?: string[];
45
+ category?: string;
44
46
  shared?: boolean;
45
47
  singleton?: boolean;
46
48
  demo?: boolean;
@@ -12,8 +12,9 @@ import {
12
12
  type Schema,
13
13
  } from "./types";
14
14
  import { systemMessage, type Parameters, type Translator } from "../editor/i18n/format";
15
+ import { matchesMediaAccept, safeFileUrl } from "../media-types";
15
16
 
16
- const SCALARS: ScalarFieldType[] = ["string", "text", "markdown", "boolean", "number", "date", "image", "url", "color", "code", "password"];
17
+ const SCALARS: ScalarFieldType[] = ["string", "text", "markdown", "boolean", "number", "date", "image", "file", "url", "color", "code", "password"];
17
18
  const UNSAFE = new Set(["__proto__", "constructor", "prototype"]);
18
19
  const FOLDER_PART = /^[a-zA-Z0-9_-][a-zA-Z0-9_. -]*$/;
19
20
 
@@ -54,6 +55,7 @@ export function validateSchema(input: Schema): Schema {
54
55
  if (doc.demo !== undefined && typeof doc.demo !== "boolean") throw new SchemaError(`Document type ${doc.name}: demo must be boolean`);
55
56
  if (doc.creatable !== undefined && typeof doc.creatable !== "boolean") throw new SchemaError(`Document type ${doc.name}: creatable must be boolean`);
56
57
  if (typeof doc.title !== "string" || !doc.title.trim()) throw new SchemaError(`Document type ${doc.name} requires a title`);
58
+ if (doc.category !== undefined && (typeof doc.category !== "string" || !doc.category.trim())) throw new SchemaError(`Document type ${doc.name}: category must be a non-empty string`);
57
59
  const parts = String(doc.folder).split("/");
58
60
  if (!parts.length || parts.some((part) => !FOLDER_PART.test(part))) throw new SchemaError(`Document type ${doc.name} has an unsafe folder`);
59
61
  if (folders.has(doc.folder)) throw new SchemaError(`Duplicate document folder: ${doc.folder}`);
@@ -88,6 +90,7 @@ function validateFieldsShape(schema: Schema, fields: FieldDef[], scope: string)
88
90
  if (keys.has(field.key)) throw new SchemaError(`${scope}: duplicate field key ${field.key}`);
89
91
  keys.add(field.key);
90
92
  if (typeof field.label !== "string" || !field.label.trim()) throw new SchemaError(`${scope}.${field.key}: label required`);
93
+ if (field.accept !== undefined && (field.type !== "file" || typeof field.accept !== "string" || !field.accept.split(",").every(token => /^(?:\.[a-z0-9]+|[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*))$/i.test(token.trim())))) throw new SchemaError(`${scope}.${field.key}: accept must contain file extensions or MIME types`);
91
94
  if (![...SCALARS, "object", "array", "reference"].includes(field.type)) throw new SchemaError(`${scope}.${field.key}: unknown field type ${field.type}`);
92
95
  if (field.type === "object") {
93
96
  if (field.of) objectDef(schema, field.of);
@@ -130,6 +133,7 @@ function scalarOk(type: ScalarFieldType, value: unknown): boolean {
130
133
  if (type === "boolean") return typeof value === "boolean";
131
134
  if (type === "number") return typeof value === "number" && Number.isFinite(value);
132
135
  if (typeof value !== "string") return false;
136
+ if (type === "file") return safeFileUrl(value);
133
137
  if (type === "color") return value === "" || /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(value);
134
138
  if (type === "date") return /^\d{4}-\d{2}-\d{2}$/.test(value);
135
139
  if (type === "image" || type === "url") return safeUrl(value);
@@ -138,7 +142,7 @@ function scalarOk(type: ScalarFieldType, value: unknown): boolean {
138
142
 
139
143
  function requireValue(field: FieldDef, value: unknown, path: string, message: Translator) {
140
144
  if (value === undefined || value === null) fail(path, "validation.schema.required", message);
141
- if (typeof value === "string" && ["string", "text", "markdown", "image", "url"].includes(field.type) && !value.trim()) fail(path, "validation.schema.required", message);
145
+ if (typeof value === "string" && ["string", "text", "markdown", "image", "file", "url"].includes(field.type) && !value.trim()) fail(path, "validation.schema.required", message);
142
146
  if (field.required && Array.isArray(value) && !value.length) fail(path, "validation.schema.required", message);
143
147
  }
144
148
 
@@ -214,8 +218,9 @@ function validateFieldValue(schema: Schema, field: FieldDef, value: unknown, pat
214
218
  validateReference(schema, value, field.to ?? [], path, documents, message);
215
219
  return;
216
220
  }
217
- if ((field.type === "url" || field.type === "image") && value === "" && !field.required) return;
221
+ if ((field.type === "url" || field.type === "image" || field.type === "file") && value === "" && !field.required) return;
218
222
  if (!scalarOk(field.type as ScalarFieldType, value)) fail(path, "validation.schema.scalar", message, { type: field.type });
223
+ if (field.type === "file" && !matchesMediaAccept(value as string, field.accept)) fail(path, "validation.schema.scalar", message, { type: field.accept ?? "file" });
219
224
  }
220
225
 
221
226
  type ItemDef = { type: ScalarFieldType | "object" | "reference"; name?: string; fields?: FieldDef[]; to?: string[] };