@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.
- package/docs/content-locales.md +8 -0
- package/package.json +1 -1
- package/src/editor/App.svelte +52 -28
- package/src/editor/i18n/de/fields.json +3 -0
- package/src/editor/i18n/de/media.json +9 -1
- package/src/editor/i18n/de/shell.json +4 -0
- package/src/editor/i18n/en/fields.json +3 -0
- package/src/editor/i18n/en/media.json +9 -1
- package/src/editor/i18n/en/shell.json +4 -0
- package/src/editor/i18n/es/fields.json +3 -0
- package/src/editor/i18n/es/media.json +9 -1
- package/src/editor/i18n/es/shell.json +4 -0
- package/src/editor/i18n/fr/fields.json +3 -0
- package/src/editor/i18n/fr/media.json +9 -1
- package/src/editor/i18n/fr/shell.json +4 -0
- package/src/editor/i18n/it/fields.json +3 -0
- package/src/editor/i18n/it/media.json +9 -1
- package/src/editor/i18n/it/shell.json +4 -0
- package/src/editor/i18n/ja/fields.json +3 -0
- package/src/editor/i18n/ja/media.json +9 -1
- package/src/editor/i18n/ja/shell.json +4 -0
- package/src/editor/i18n/nl/fields.json +3 -0
- package/src/editor/i18n/nl/media.json +9 -1
- package/src/editor/i18n/nl/shell.json +4 -0
- package/src/editor/i18n/ru/fields.json +3 -0
- package/src/editor/i18n/ru/media.json +9 -1
- package/src/editor/i18n/ru/shell.json +4 -0
- package/src/editor/i18n/zh/fields.json +3 -0
- package/src/editor/i18n/zh/media.json +9 -1
- package/src/editor/i18n/zh/shell.json +4 -0
- package/src/editor/lib/ActionDialog.svelte +23 -3
- package/src/editor/lib/ImagePicker.svelte +27 -19
- package/src/editor/lib/MarkdownEditor.svelte +7 -5
- package/src/editor/lib/SchemaField.svelte +2 -2
- package/src/editor/lib/SchemaPanel.svelte +76 -4
- package/src/editor/lib/VisualEditor.svelte +38 -3
- package/src/editor/lib/icons.ts +4 -0
- package/src/editor/styles.css +15 -3
- package/src/manifest/content-locales.ts +1 -1
- package/src/manifest/navigation.ts +4 -3
- package/src/manifest/pages.ts +17 -0
- package/src/media-types.ts +17 -0
- package/src/schema/types.ts +3 -1
- package/src/schema/validate.ts +8 -3
package/src/media-types.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/schema/types.ts
CHANGED
|
@@ -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;
|
package/src/schema/validate.ts
CHANGED
|
@@ -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[] };
|