@chelone/core 0.6.0 → 0.7.0
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/dist/assistant/assistant-session.d.ts +6 -0
- package/dist/assistant/assistant-session.js +204 -181
- package/dist/assistant/echo-transport.js +26 -29
- package/dist/assistant/http-transport.d.ts +10 -0
- package/dist/assistant/http-transport.js +98 -0
- package/dist/assistant/types.d.ts +1 -7
- package/dist/auth/access-control.d.ts +13 -5
- package/dist/auth/access-control.js +25 -21
- package/dist/auth/auth-options.d.ts +21 -17
- package/dist/auth/auth-options.js +80 -48
- package/dist/auth/permission-matrix.d.ts +12 -0
- package/dist/auth/permission-matrix.js +67 -0
- package/dist/auth/types.d.ts +14 -4
- package/dist/block-editor/block-document.d.ts +60 -0
- package/dist/block-editor/block-document.js +143 -0
- package/dist/block-editor/data-blocks.d.ts +63 -0
- package/dist/block-editor/data-blocks.js +117 -0
- package/dist/chart/chart-data.js +46 -44
- package/dist/command-menu/hotkey.js +58 -47
- package/dist/command-menu/recents.js +8 -11
- package/dist/command-menu/registry.d.ts +9 -5
- package/dist/command-menu/registry.js +36 -46
- package/dist/controller/calendar-dates.js +82 -113
- package/dist/controller/calendar-events.js +93 -88
- package/dist/controller/client-list-controller.d.ts +2 -2
- package/dist/controller/client-list-controller.js +251 -254
- package/dist/controller/data-table-columns.d.ts +2 -0
- package/dist/controller/data-table-columns.js +50 -56
- package/dist/controller/deep-equal.d.ts +1 -0
- package/dist/controller/deep-equal.js +14 -0
- package/dist/controller/filter-toggle.js +36 -44
- package/dist/controller/in-place-editor.d.ts +42 -0
- package/dist/controller/in-place-editor.js +42 -0
- package/dist/controller/infinite-list-controller.js +219 -189
- package/dist/controller/inspector.d.ts +49 -0
- package/dist/controller/inspector.js +64 -0
- package/dist/controller/job-monitor.d.ts +3 -4
- package/dist/controller/job-monitor.js +37 -47
- package/dist/controller/kanban-board.d.ts +2 -3
- package/dist/controller/kanban-board.js +43 -24
- package/dist/controller/list-controller.d.ts +5 -5
- package/dist/controller/list-controller.js +444 -400
- package/dist/controller/list-params.js +96 -87
- package/dist/controller/pivot-grid.js +89 -95
- package/dist/controller/prev-next.d.ts +2 -2
- package/dist/controller/prev-next.js +74 -54
- package/dist/controller/record-controllers.d.ts +14 -6
- package/dist/controller/record-controllers.js +288 -257
- package/dist/controller/record-diff.d.ts +21 -0
- package/dist/controller/record-diff.js +50 -0
- package/dist/controller/record-timeline.js +42 -30
- package/dist/controller/record-tree.js +57 -47
- package/dist/controller/record-workflow.d.ts +38 -16
- package/dist/controller/record-workflow.js +85 -64
- package/dist/controller/reference-many-controller.js +198 -192
- package/dist/controller/reference-one-controller.d.ts +1 -1
- package/dist/controller/reference-one-controller.js +63 -56
- package/dist/controller/saved-queries.js +104 -91
- package/dist/controller/translate-fallback.js +10 -6
- package/dist/csv/export.d.ts +2 -1
- package/dist/csv/export.js +43 -40
- package/dist/csv/import-controller.js +185 -169
- package/dist/csv/match-headers.js +29 -22
- package/dist/csv/parse-with.js +60 -54
- package/dist/csv/run-import.js +69 -72
- package/dist/csv/validate-rows.d.ts +1 -1
- package/dist/csv/validate-rows.js +65 -52
- package/dist/data/cache-ops.d.ts +5 -4
- package/dist/data/cache-ops.js +208 -188
- package/dist/data/combine-data-providers.js +25 -23
- package/dist/data/data-provider-log.d.ts +45 -0
- package/dist/data/data-provider-log.js +93 -0
- package/dist/data/errors.js +14 -10
- package/dist/data/fake-provider.d.ts +6 -3
- package/dist/data/fake-provider.js +129 -114
- package/dist/data/fetch-utils.js +61 -44
- package/dist/data/filter-branch.d.ts +5 -0
- package/dist/data/filter-branch.js +10 -0
- package/dist/data/get-many-aggregate.js +84 -76
- package/dist/data/http-error.js +17 -12
- package/dist/data/keys.js +50 -36
- package/dist/data/mutation-options.d.ts +14 -9
- package/dist/data/mutation-options.js +93 -109
- package/dist/data/query-options.js +73 -68
- package/dist/data/remove-empty.d.ts +3 -0
- package/dist/data/remove-empty.js +11 -0
- package/dist/data/undoable.d.ts +2 -2
- package/dist/data/undoable.js +190 -174
- package/dist/data/with-lifecycle-callbacks.d.ts +45 -17
- package/dist/data/with-lifecycle-callbacks.js +66 -52
- package/dist/environment/application-update-store.js +58 -55
- package/dist/environment/offline-store.js +21 -21
- package/dist/form/array-binding.d.ts +30 -11
- package/dist/form/array-binding.js +184 -166
- package/dist/form/choice-ids.d.ts +7 -0
- package/dist/form/choice-ids.js +11 -0
- package/dist/form/choices-controller.d.ts +3 -2
- package/dist/form/choices-controller.js +302 -275
- package/dist/form/date-format.d.ts +38 -0
- package/dist/form/date-format.js +89 -0
- package/dist/form/dirty-values.d.ts +2 -2
- package/dist/form/dirty-values.js +22 -11
- package/dist/form/duration.d.ts +22 -0
- package/dist/form/duration.js +49 -0
- package/dist/form/edit-create-form.d.ts +12 -5
- package/dist/form/edit-create-form.js +243 -188
- package/dist/form/event-value.d.ts +6 -0
- package/dist/form/event-value.js +17 -0
- package/dist/form/field-error.d.ts +14 -0
- package/dist/form/field-error.js +28 -0
- package/dist/form/field-name.d.ts +20 -0
- package/dist/form/field-name.js +6 -0
- package/dist/form/filter-form-values.js +63 -49
- package/dist/form/form-controller.d.ts +9 -1
- package/dist/form/form-controller.js +165 -160
- package/dist/form/form-groups.d.ts +4 -3
- package/dist/form/form-groups.js +79 -79
- package/dist/form/form-handle.d.ts +175 -0
- package/dist/form/form-handle.js +68 -0
- package/dist/form/input-binding.d.ts +56 -31
- package/dist/form/input-binding.js +169 -118
- package/dist/form/input-values.d.ts +130 -0
- package/dist/form/input-values.js +177 -0
- package/dist/form/json-text.js +51 -37
- package/dist/form/save-pipeline.d.ts +6 -8
- package/dist/form/save-pipeline.js +91 -82
- package/dist/form/set-form-values.d.ts +6 -0
- package/dist/form/set-form-values.js +10 -0
- package/dist/form/source.js +97 -65
- package/dist/form/text-of.d.ts +2 -0
- package/dist/form/text-of.js +7 -0
- package/dist/form/validators.d.ts +58 -19
- package/dist/form/validators.js +97 -71
- package/dist/geo/bbox.d.ts +5 -4
- package/dist/geo/bbox.js +69 -48
- package/dist/geo/bounds.d.ts +14 -3
- package/dist/geo/bounds.js +98 -68
- package/dist/geo/constraints.js +88 -93
- package/dist/geo/coordinates.js +31 -23
- package/dist/geo/editor.d.ts +13 -13
- package/dist/geo/editor.js +176 -153
- package/dist/geo/geocoding.d.ts +4 -4
- package/dist/geo/geocoding.js +38 -41
- package/dist/geo/geojson.d.ts +12 -74
- package/dist/geo/geojson.js +52 -40
- package/dist/geo/geometry-math.d.ts +4 -4
- package/dist/geo/geometry-math.js +100 -93
- package/dist/geo/index.d.ts +7 -6
- package/dist/geo/map-layers.d.ts +42 -7
- package/dist/geo/map-layers.js +88 -62
- package/dist/geo/map-style.js +31 -32
- package/dist/geo/messages.js +48 -45
- package/dist/geo/nominatim.js +88 -80
- package/dist/geo/osm-presets.d.ts +4 -3
- package/dist/geo/osm-presets.js +108 -86
- package/dist/geo/osm-tags.js +14 -12
- package/dist/geo/overpass.d.ts +14 -2
- package/dist/geo/overpass.js +83 -90
- package/dist/geo/shape-value.d.ts +9 -5
- package/dist/geo/shape-value.js +169 -146
- package/dist/geo/snap-to-roads.d.ts +3 -3
- package/dist/geo/snap-to-roads.js +55 -50
- package/dist/geo/throttle.js +36 -39
- package/dist/guards.d.ts +37 -0
- package/dist/guards.js +62 -0
- package/dist/i18n/default-messages.js +34 -29
- package/dist/i18n/english-messages.js +187 -189
- package/dist/i18n/i18n.d.ts +2 -2
- package/dist/i18n/i18n.js +47 -60
- package/dist/i18n/inflection.d.ts +34 -0
- package/dist/i18n/inflection.js +82 -0
- package/dist/i18n/messages.d.ts +10 -2
- package/dist/i18n/messages.js +17 -4
- package/dist/i18n/missing-keys.js +66 -62
- package/dist/i18n/translatable.d.ts +16 -1
- package/dist/i18n/translatable.js +40 -47
- package/dist/index.d.ts +71 -24
- package/dist/index.js +40 -288
- package/dist/json/deep-equal.d.ts +8 -0
- package/dist/json/deep-equal.js +24 -0
- package/dist/json/strip-json-comments.js +38 -42
- package/dist/noop.d.ts +2 -0
- package/dist/noop.js +4 -0
- package/dist/notification/error-message.d.ts +2 -0
- package/dist/notification/error-message.js +10 -0
- package/dist/notification/notification-store.d.ts +3 -2
- package/dist/notification/notification-store.js +41 -45
- package/dist/onboarding-tour/placement.js +63 -78
- package/dist/onboarding-tour/tour-controller.js +46 -54
- package/dist/realtime/add-events-for-mutations.js +75 -81
- package/dist/realtime/announce-lock.js +16 -13
- package/dist/realtime/broadcast-channel-transport.js +65 -59
- package/dist/realtime/copy-members.js +22 -14
- package/dist/realtime/in-memory-lock-provider.js +40 -48
- package/dist/realtime/live.js +95 -101
- package/dist/realtime/lock-options.js +16 -16
- package/dist/realtime/lock-session.js +91 -78
- package/dist/realtime/memory-transport.js +25 -25
- package/dist/realtime/presence.js +120 -122
- package/dist/realtime/realtime-data-provider.d.ts +10 -0
- package/dist/realtime/realtime-data-provider.js +45 -42
- package/dist/realtime/reconnect-backoff.js +17 -0
- package/dist/realtime/sse-transport.d.ts +31 -0
- package/dist/realtime/sse-transport.js +235 -0
- package/dist/realtime/status-store.js +23 -28
- package/dist/realtime/topic-hub.js +62 -71
- package/dist/realtime/topics.js +10 -12
- package/dist/realtime/types.js +10 -12
- package/dist/realtime/unread-count.js +21 -21
- package/dist/realtime/websocket-transport.d.ts +32 -0
- package/dist/realtime/websocket-transport.js +306 -0
- package/dist/resource/describe-resource.d.ts +2 -0
- package/dist/resource/describe-resource.js +232 -219
- package/dist/resource/get-at-path.js +29 -14
- package/dist/resource/identifier.d.ts +18 -0
- package/dist/resource/identifier.js +27 -0
- package/dist/resource/record.d.ts +4 -0
- package/dist/resource/record.js +10 -0
- package/dist/resource/register.d.ts +7 -0
- package/dist/resource/registry-store.d.ts +45 -15
- package/dist/resource/registry-store.js +67 -60
- package/dist/resource/registry.d.ts +10 -0
- package/dist/resource/registry.js +8 -4
- package/dist/resource/source-path.d.ts +16 -1
- package/dist/rich-text/rich-text-extras.d.ts +132 -0
- package/dist/rich-text/rich-text-extras.js +179 -0
- package/dist/rich-text/rich-text-image.d.ts +124 -0
- package/dist/rich-text/rich-text-image.js +163 -0
- package/dist/rich-text/rich-text.d.ts +6 -0
- package/dist/rich-text/rich-text.js +125 -88
- package/dist/router/location.d.ts +22 -4
- package/dist/router/location.js +41 -18
- package/dist/router/match.js +47 -39
- package/dist/router/paths.js +40 -34
- package/dist/router/record-from-location.d.ts +1 -1
- package/dist/router/record-from-location.js +25 -17
- package/dist/store/adapters.js +51 -55
- package/dist/store/micro-store.js +57 -59
- package/dist/store/shallow-equal.d.ts +12 -0
- package/dist/store/shallow-equal.js +20 -0
- package/dist/store/store.js +54 -57
- package/package.json +13 -11
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { type RichTextImageRules, type RichTextUpload } from "./rich-text-extras.js";
|
|
2
|
+
/** What `uploadFn` may resolve to: a URL, a URL with the stored file's id, or `null` for a failed upload. */
|
|
3
|
+
export type RichTextUploadResult = string | {
|
|
4
|
+
id: string | number;
|
|
5
|
+
src: string;
|
|
6
|
+
} | null;
|
|
7
|
+
export type RichTextImageErrorReason =
|
|
8
|
+
/** The file type is not allowed. */
|
|
9
|
+
"type"
|
|
10
|
+
/** The file is over `maxFileSize`. */
|
|
11
|
+
| "size"
|
|
12
|
+
/** The URL is not a safe image source. */
|
|
13
|
+
| "src"
|
|
14
|
+
/** The upload threw, returned `null`, or returned an unsafe URL. */
|
|
15
|
+
| "upload";
|
|
16
|
+
export interface RichTextImageError {
|
|
17
|
+
reason: RichTextImageErrorReason;
|
|
18
|
+
file: File | null;
|
|
19
|
+
/** What `uploadFn` threw, for an `upload` error. */
|
|
20
|
+
cause?: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** The image settings of `<RichTextInput images={...}>`: core's rules, each optional, plus the two callbacks. */
|
|
23
|
+
export interface RichTextImageOptions extends Partial<RichTextImageRules> {
|
|
24
|
+
/**
|
|
25
|
+
* Stores a file and resolves to its URL. Without it a file is stored in the
|
|
26
|
+
* HTML as a data URL when `allowBase64` is on.
|
|
27
|
+
*/
|
|
28
|
+
uploadFn?: (file: File) => Promise<RichTextUploadResult>;
|
|
29
|
+
/** Called for a file or URL that is refused, and for an upload that fails. */
|
|
30
|
+
onImageError?: (error: RichTextImageError) => void;
|
|
31
|
+
}
|
|
32
|
+
export interface RichTextImageConfig {
|
|
33
|
+
rules: RichTextImageRules;
|
|
34
|
+
uploadFn: RichTextImageOptions["uploadFn"];
|
|
35
|
+
onImageError: RichTextImageOptions["onImageError"];
|
|
36
|
+
}
|
|
37
|
+
/** `images` from the input's props, with core's defaults under every rule. */
|
|
38
|
+
export declare function resolveRichTextImageConfig(images: boolean | RichTextImageOptions): RichTextImageConfig;
|
|
39
|
+
export type RichTextImageInput = File | {
|
|
40
|
+
src: string;
|
|
41
|
+
alt?: string;
|
|
42
|
+
title?: string;
|
|
43
|
+
};
|
|
44
|
+
/** What a finished upload left on its image: the stored source, or `null` when it failed. */
|
|
45
|
+
export interface RichTextStoredImage {
|
|
46
|
+
src: string | null;
|
|
47
|
+
id: string | null;
|
|
48
|
+
}
|
|
49
|
+
interface UploadState {
|
|
50
|
+
pending: ReadonlySet<string>;
|
|
51
|
+
settled: Map<string, RichTextStoredImage>;
|
|
52
|
+
listeners: Set<() => void>;
|
|
53
|
+
}
|
|
54
|
+
export declare function richTextImageUploadState(editor: object): UploadState;
|
|
55
|
+
export declare function setRichTextImageUploading(editor: object, src: string, uploading: boolean): void;
|
|
56
|
+
export declare function storeRichTextImageFile(file: File, { rules, uploadFn }: RichTextImageConfig): Promise<RichTextUpload>;
|
|
57
|
+
export declare function downloadRichTextImage(src: string, alt: string): Promise<void>;
|
|
58
|
+
export declare function copyRichTextImage(src: string): Promise<void>;
|
|
59
|
+
/** DOMPurify's image hook, shared by both fields on their own sanitizer instances. */
|
|
60
|
+
export declare function sanitizeRichTextImageElement(node: Node): void;
|
|
61
|
+
interface PreparedImage {
|
|
62
|
+
src: string;
|
|
63
|
+
alt?: string;
|
|
64
|
+
title?: string;
|
|
65
|
+
fileName?: string;
|
|
66
|
+
}
|
|
67
|
+
/** Validate a batch and prepare its temporary previews before an adapter inserts it. */
|
|
68
|
+
export declare function prepareRichTextImages(inputs: readonly RichTextImageInput[], config: RichTextImageConfig): {
|
|
69
|
+
attributes: PreparedImage[];
|
|
70
|
+
uploads: {
|
|
71
|
+
file: File;
|
|
72
|
+
blobUrl: string;
|
|
73
|
+
}[];
|
|
74
|
+
};
|
|
75
|
+
/** Extra image attributes in the stored HTML, with finite dimensions only. */
|
|
76
|
+
export declare function richTextImageAttributes(): {
|
|
77
|
+
id: {
|
|
78
|
+
default: null;
|
|
79
|
+
parseHTML: (element: HTMLElement) => string | null;
|
|
80
|
+
renderHTML: (attrs: {
|
|
81
|
+
id?: string | number | null;
|
|
82
|
+
}) => {
|
|
83
|
+
"data-id"?: undefined;
|
|
84
|
+
} | {
|
|
85
|
+
"data-id": string;
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
fileName: {
|
|
89
|
+
default: null;
|
|
90
|
+
parseHTML: (element: HTMLElement) => string | null;
|
|
91
|
+
renderHTML: (attrs: {
|
|
92
|
+
fileName?: string | null;
|
|
93
|
+
}) => {
|
|
94
|
+
"data-file-name"?: undefined;
|
|
95
|
+
} | {
|
|
96
|
+
"data-file-name": string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
width: {
|
|
100
|
+
default: null;
|
|
101
|
+
parseHTML: (element: HTMLElement) => number | null;
|
|
102
|
+
renderHTML: (attrs: {
|
|
103
|
+
width?: number | null;
|
|
104
|
+
height?: number | null;
|
|
105
|
+
}) => {
|
|
106
|
+
[x: string]: number | null | undefined;
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
height: {
|
|
110
|
+
default: null;
|
|
111
|
+
parseHTML: (element: HTMLElement) => number | null;
|
|
112
|
+
renderHTML: (attrs: {
|
|
113
|
+
width?: number | null;
|
|
114
|
+
height?: number | null;
|
|
115
|
+
}) => {
|
|
116
|
+
[x: string]: number | null | undefined;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
/** Narrows options from the editor library's untyped extension boundary. */
|
|
121
|
+
export declare function isRichTextImageConfigSource(options: unknown): options is {
|
|
122
|
+
config: () => RichTextImageConfig;
|
|
123
|
+
};
|
|
124
|
+
export {};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { isSafeImageSrc } from "./rich-text.js";
|
|
2
|
+
import { checkRichTextImageFile, defaultRichTextImageRules, normalizeRichTextUpload, richTextImageFileName } from "./rich-text-extras.js";
|
|
3
|
+
// src/rich-text/rich-text-image.ts
|
|
4
|
+
/** `images` from the input's props, with core's defaults under every rule. */
|
|
5
|
+
function resolveRichTextImageConfig(images) {
|
|
6
|
+
const options = typeof images === "object" ? images : {};
|
|
7
|
+
return {
|
|
8
|
+
rules: {
|
|
9
|
+
allowedMimeTypes: options.allowedMimeTypes ?? defaultRichTextImageRules.allowedMimeTypes,
|
|
10
|
+
maxFileSize: options.maxFileSize ?? defaultRichTextImageRules.maxFileSize,
|
|
11
|
+
allowBase64: options.allowBase64 ?? defaultRichTextImageRules.allowBase64
|
|
12
|
+
},
|
|
13
|
+
uploadFn: options.uploadFn,
|
|
14
|
+
onImageError: options.onImageError
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
var uploadStates = /* @__PURE__ */ new WeakMap();
|
|
18
|
+
function richTextImageUploadState(editor) {
|
|
19
|
+
const existing = uploadStates.get(editor);
|
|
20
|
+
if (existing) return existing;
|
|
21
|
+
const created = {
|
|
22
|
+
pending: /* @__PURE__ */ new Set(),
|
|
23
|
+
settled: /* @__PURE__ */ new Map(),
|
|
24
|
+
listeners: /* @__PURE__ */ new Set()
|
|
25
|
+
};
|
|
26
|
+
uploadStates.set(editor, created);
|
|
27
|
+
return created;
|
|
28
|
+
}
|
|
29
|
+
function setRichTextImageUploading(editor, src, uploading) {
|
|
30
|
+
const state = richTextImageUploadState(editor);
|
|
31
|
+
const pending = new Set(state.pending);
|
|
32
|
+
if (uploading) pending.add(src);
|
|
33
|
+
else pending.delete(src);
|
|
34
|
+
state.pending = pending;
|
|
35
|
+
for (const listener of state.listeners) listener();
|
|
36
|
+
}
|
|
37
|
+
function readDataUrl(file) {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const reader = new FileReader();
|
|
40
|
+
reader.onload = () => typeof reader.result === "string" ? resolve(reader.result) : reject(/* @__PURE__ */ new Error("The file could not be read"));
|
|
41
|
+
reader.onerror = () => reject(reader.error);
|
|
42
|
+
reader.readAsDataURL(file);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/** Without `uploadFn` the file becomes a data URL, which only some raster types may be. */
|
|
46
|
+
function canBeDataUrl(file, allowBase64) {
|
|
47
|
+
const type = (file.type.split(";")[0] ?? "").trim();
|
|
48
|
+
return !allowBase64 || isSafeImageSrc(`data:${type};base64,`, { allowDataImages: true });
|
|
49
|
+
}
|
|
50
|
+
async function storeRichTextImageFile(file, { rules, uploadFn }) {
|
|
51
|
+
const result = uploadFn ? await uploadFn(file) : rules.allowBase64 ? await readDataUrl(file) : null;
|
|
52
|
+
const upload = result === null ? null : normalizeRichTextUpload(result, rules);
|
|
53
|
+
if (upload === null) throw new Error("No safe image source was stored");
|
|
54
|
+
return upload;
|
|
55
|
+
}
|
|
56
|
+
function triggerDownload(href, fileName) {
|
|
57
|
+
const anchor = document.createElement("a");
|
|
58
|
+
anchor.href = href;
|
|
59
|
+
anchor.download = fileName;
|
|
60
|
+
anchor.rel = "noopener";
|
|
61
|
+
anchor.click();
|
|
62
|
+
}
|
|
63
|
+
async function downloadRichTextImage(src, alt) {
|
|
64
|
+
try {
|
|
65
|
+
const blob = await (await fetch(src)).blob();
|
|
66
|
+
const url = URL.createObjectURL(blob);
|
|
67
|
+
triggerDownload(url, richTextImageFileName(alt, blob.type));
|
|
68
|
+
setTimeout(() => URL.revokeObjectURL(url));
|
|
69
|
+
} catch {
|
|
70
|
+
triggerDownload(src, richTextImageFileName(alt, ""));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Browsers take only PNG on the clipboard, so other formats are redrawn as one. */
|
|
74
|
+
async function toPng(blob) {
|
|
75
|
+
if (blob.type === "image/png") return blob;
|
|
76
|
+
const bitmap = await createImageBitmap(blob);
|
|
77
|
+
const canvas = document.createElement("canvas");
|
|
78
|
+
canvas.width = bitmap.width;
|
|
79
|
+
canvas.height = bitmap.height;
|
|
80
|
+
canvas.getContext("2d")?.drawImage(bitmap, 0, 0);
|
|
81
|
+
return new Promise((resolve, reject) => canvas.toBlob((png) => png ? resolve(png) : reject(/* @__PURE__ */ new Error("No PNG was produced")), "image/png"));
|
|
82
|
+
}
|
|
83
|
+
async function copyRichTextImage(src) {
|
|
84
|
+
const blob = await toPng(await (await fetch(src)).blob());
|
|
85
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
|
86
|
+
}
|
|
87
|
+
/** DOMPurify's image hook, shared by both fields on their own sanitizer instances. */
|
|
88
|
+
function sanitizeRichTextImageElement(node) {
|
|
89
|
+
if (node.nodeName !== "IMG" || !(node instanceof window.Element)) return;
|
|
90
|
+
const src = node.getAttribute("src");
|
|
91
|
+
if (src === null || !isSafeImageSrc(src, { allowDataImages: true })) node.remove();
|
|
92
|
+
}
|
|
93
|
+
/** Validate a batch and prepare its temporary previews before an adapter inserts it. */
|
|
94
|
+
function prepareRichTextImages(inputs, config) {
|
|
95
|
+
const attributes = [];
|
|
96
|
+
const uploads = [];
|
|
97
|
+
const { rules, onImageError } = config;
|
|
98
|
+
for (const input of inputs) {
|
|
99
|
+
if (!(input instanceof File)) {
|
|
100
|
+
if (isSafeImageSrc(input.src, { allowDataImages: rules.allowBase64 })) attributes.push(input);
|
|
101
|
+
else onImageError?.({
|
|
102
|
+
reason: "src",
|
|
103
|
+
file: null
|
|
104
|
+
});
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const reason = checkRichTextImageFile(input, rules) ?? (config.uploadFn === void 0 && !canBeDataUrl(input, rules.allowBase64) ? "type" : null);
|
|
108
|
+
if (reason) {
|
|
109
|
+
onImageError?.({
|
|
110
|
+
reason,
|
|
111
|
+
file: input
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const blobUrl = URL.createObjectURL(input);
|
|
116
|
+
uploads.push({
|
|
117
|
+
file: input,
|
|
118
|
+
blobUrl
|
|
119
|
+
});
|
|
120
|
+
attributes.push({
|
|
121
|
+
src: blobUrl,
|
|
122
|
+
alt: input.name.replace(/\.[^.]*$/, ""),
|
|
123
|
+
fileName: input.name
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
attributes,
|
|
128
|
+
uploads
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function imageDimension(value) {
|
|
132
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
133
|
+
}
|
|
134
|
+
/** Extra image attributes in the stored HTML, with finite dimensions only. */
|
|
135
|
+
function richTextImageAttributes() {
|
|
136
|
+
const dimension = (key) => ({
|
|
137
|
+
default: null,
|
|
138
|
+
parseHTML: (element) => {
|
|
139
|
+
const raw = element.getAttribute(key);
|
|
140
|
+
return raw === null ? null : imageDimension(Number.parseFloat(raw));
|
|
141
|
+
},
|
|
142
|
+
renderHTML: (attrs) => imageDimension(attrs[key]) === null ? {} : { [key]: attrs[key] }
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
id: {
|
|
146
|
+
default: null,
|
|
147
|
+
parseHTML: (element) => element.getAttribute("data-id"),
|
|
148
|
+
renderHTML: (attrs) => attrs.id == null ? {} : { "data-id": String(attrs.id) }
|
|
149
|
+
},
|
|
150
|
+
fileName: {
|
|
151
|
+
default: null,
|
|
152
|
+
parseHTML: (element) => element.getAttribute("data-file-name"),
|
|
153
|
+
renderHTML: (attrs) => attrs.fileName == null ? {} : { "data-file-name": String(attrs.fileName) }
|
|
154
|
+
},
|
|
155
|
+
width: dimension("width"),
|
|
156
|
+
height: dimension("height")
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Narrows options from the editor library's untyped extension boundary. */
|
|
160
|
+
function isRichTextImageConfigSource(options) {
|
|
161
|
+
return typeof options === "object" && options !== null && "config" in options && typeof options.config === "function";
|
|
162
|
+
}
|
|
163
|
+
export { copyRichTextImage, downloadRichTextImage, isRichTextImageConfigSource, prepareRichTextImages, resolveRichTextImageConfig, richTextImageAttributes, richTextImageUploadState, sanitizeRichTextImageElement, setRichTextImageUploading, storeRichTextImageFile };
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
/** Whether a link `href` is safe to store and render in rich text. */
|
|
2
2
|
export declare function isSafeLinkUrl(url: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Whether a file `href` is safe to render. `blob` is allowed on top of the link
|
|
5
|
+
* schemes: `FileInput` previews a picked file with `URL.createObjectURL` and
|
|
6
|
+
* hands that url to `FileField`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function isSafeFileUrl(url: string): boolean;
|
|
3
9
|
export interface IsSafeImageSrcOptions {
|
|
4
10
|
/** Allow base64 raster `data:image/*` sources. SVG is never allowed. */
|
|
5
11
|
allowDataImages?: boolean;
|
|
@@ -1,105 +1,142 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// src/rich-text/rich-text.ts
|
|
2
|
+
var CONTROL = /[\u0000-\u001f\u007f]/g;
|
|
3
|
+
var SCHEME = /^([a-z][a-z0-9+.-]*):/i;
|
|
4
|
+
var LINK_SCHEMES = /* @__PURE__ */ new Set([
|
|
5
|
+
"http",
|
|
6
|
+
"https",
|
|
7
|
+
"mailto",
|
|
8
|
+
"tel"
|
|
8
9
|
]);
|
|
10
|
+
/**
|
|
11
|
+
* `url` as a browser reads its scheme. Browsers drop tabs and newlines anywhere in a URL and
|
|
12
|
+
* ignore leading control characters and spaces, so `java\tscript:` is
|
|
13
|
+
* `javascript:`. Every control character is dropped here, which can only make a
|
|
14
|
+
* scheme easier to see.
|
|
15
|
+
*/
|
|
9
16
|
function normalize(url) {
|
|
10
|
-
|
|
17
|
+
return url.replace(CONTROL, "").trimStart();
|
|
11
18
|
}
|
|
12
19
|
function schemeOf(normalized) {
|
|
13
|
-
|
|
20
|
+
return SCHEME.exec(normalized)?.[1]?.toLowerCase();
|
|
14
21
|
}
|
|
22
|
+
/** Whether a link `href` is safe to store and render in rich text. */
|
|
15
23
|
function isSafeLinkUrl(url) {
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
const scheme = schemeOf(normalize(url));
|
|
25
|
+
return scheme === void 0 || LINK_SCHEMES.has(scheme);
|
|
18
26
|
}
|
|
19
|
-
|
|
20
|
-
|
|
27
|
+
var FILE_SCHEMES = /* @__PURE__ */ new Set([
|
|
28
|
+
"http",
|
|
29
|
+
"https",
|
|
30
|
+
"blob"
|
|
31
|
+
]);
|
|
32
|
+
/**
|
|
33
|
+
* Whether a file `href` is safe to render. `blob` is allowed on top of the link
|
|
34
|
+
* schemes: `FileInput` previews a picked file with `URL.createObjectURL` and
|
|
35
|
+
* hands that url to `FileField`.
|
|
36
|
+
*/
|
|
37
|
+
function isSafeFileUrl(url) {
|
|
38
|
+
const scheme = schemeOf(normalize(url));
|
|
39
|
+
return scheme === void 0 || FILE_SCHEMES.has(scheme);
|
|
40
|
+
}
|
|
41
|
+
var IMAGE_SCHEMES = /* @__PURE__ */ new Set([
|
|
42
|
+
"http",
|
|
43
|
+
"https",
|
|
44
|
+
"blob"
|
|
45
|
+
]);
|
|
46
|
+
var RASTER_DATA_URL = /^data:image\/(?:png|jpeg|gif|webp|avif);base64,/i;
|
|
47
|
+
/** Whether an image `src` is safe to store and render in rich text. */
|
|
21
48
|
function isSafeImageSrc(src, options = {}) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
49
|
+
const normalized = normalize(src);
|
|
50
|
+
const scheme = schemeOf(normalized);
|
|
51
|
+
if (scheme === void 0 || IMAGE_SCHEMES.has(scheme)) return true;
|
|
52
|
+
return options.allowDataImages === true && RASTER_DATA_URL.test(normalized);
|
|
26
53
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
54
|
+
/** DOMPurify config for stored rich text HTML. Spread it into `sanitize`. */
|
|
55
|
+
var richTextSanitizeOptions = Object.freeze({
|
|
56
|
+
FORBID_TAGS: Object.freeze([
|
|
57
|
+
"style",
|
|
58
|
+
"form",
|
|
59
|
+
"input",
|
|
60
|
+
"button",
|
|
61
|
+
"textarea",
|
|
62
|
+
"select",
|
|
63
|
+
"iframe",
|
|
64
|
+
"object",
|
|
65
|
+
"embed"
|
|
66
|
+
]),
|
|
67
|
+
FORBID_ATTR: Object.freeze(["style"])
|
|
40
68
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Options for tiptap's `StarterKit.configure`. `isAllowedUri` ignores tiptap's
|
|
71
|
+
* second argument, so the scheme rule is ours alone.
|
|
72
|
+
*/
|
|
73
|
+
var richTextStarterKitOptions = Object.freeze({
|
|
74
|
+
heading: Object.freeze({ levels: Object.freeze([
|
|
75
|
+
1,
|
|
76
|
+
2,
|
|
77
|
+
3,
|
|
78
|
+
4,
|
|
79
|
+
5,
|
|
80
|
+
6
|
|
81
|
+
]) }),
|
|
82
|
+
link: Object.freeze({
|
|
83
|
+
openOnClick: false,
|
|
84
|
+
autolink: true,
|
|
85
|
+
defaultProtocol: "https",
|
|
86
|
+
isAllowedUri: (url) => isSafeLinkUrl(url)
|
|
87
|
+
})
|
|
51
88
|
});
|
|
52
89
|
function item(id, group, defaultLabel, shortcut) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
id,
|
|
92
|
+
group,
|
|
93
|
+
labelKey: `chelone.rich_text.${id}`,
|
|
94
|
+
defaultLabel,
|
|
95
|
+
...shortcut ? { shortcut } : {}
|
|
96
|
+
});
|
|
60
97
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Every toolbar button, in display order. Adapters map each id to a tiptap
|
|
100
|
+
* command, so core names no tiptap type. A shortcut is listed only where
|
|
101
|
+
* tiptap's own keymap binds it, so the adapter wires nothing extra. Link has
|
|
102
|
+
* none: tiptap binds no key for it, and `mod+k` opens the command menu.
|
|
103
|
+
*/
|
|
104
|
+
var richTextToolbarItems = Object.freeze([
|
|
105
|
+
item("bold", "mark", "Bold", "mod+b"),
|
|
106
|
+
item("italic", "mark", "Italic", "mod+i"),
|
|
107
|
+
item("underline", "mark", "Underline", "mod+u"),
|
|
108
|
+
item("strike", "mark", "Strikethrough", "mod+shift+s"),
|
|
109
|
+
item("code", "mark", "Code", "mod+e"),
|
|
110
|
+
item("clearFormatting", "mark", "Clear formatting"),
|
|
111
|
+
item("link", "insert", "Link"),
|
|
112
|
+
item("bulletList", "list", "Bullet list", "mod+shift+8"),
|
|
113
|
+
item("orderedList", "list", "Ordered list", "mod+shift+7"),
|
|
114
|
+
item("blockquote", "block", "Blockquote", "mod+shift+b"),
|
|
115
|
+
item("codeBlock", "block", "Code block", "mod+alt+c"),
|
|
116
|
+
item("horizontalRule", "insert", "Horizontal rule"),
|
|
117
|
+
item("undo", "history", "Undo", "mod+z"),
|
|
118
|
+
item("redo", "history", "Redo", "mod+shift+z")
|
|
76
119
|
]);
|
|
77
120
|
function heading(level) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
121
|
+
return Object.freeze({
|
|
122
|
+
id: `heading${level}`,
|
|
123
|
+
level,
|
|
124
|
+
labelKey: `chelone.rich_text.heading_${level}`,
|
|
125
|
+
defaultLabel: `Heading ${level}`
|
|
126
|
+
});
|
|
84
127
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
/** What the block-type select offers: paragraph, then headings 1 to 6. */
|
|
129
|
+
var richTextBlockTypes = Object.freeze([
|
|
130
|
+
Object.freeze({
|
|
131
|
+
id: "paragraph",
|
|
132
|
+
labelKey: "chelone.rich_text.paragraph",
|
|
133
|
+
defaultLabel: "Paragraph"
|
|
134
|
+
}),
|
|
135
|
+
heading(1),
|
|
136
|
+
heading(2),
|
|
137
|
+
heading(3),
|
|
138
|
+
heading(4),
|
|
139
|
+
heading(5),
|
|
140
|
+
heading(6)
|
|
97
141
|
]);
|
|
98
|
-
export {
|
|
99
|
-
isSafeImageSrc,
|
|
100
|
-
isSafeLinkUrl,
|
|
101
|
-
richTextBlockTypes,
|
|
102
|
-
richTextSanitizeOptions,
|
|
103
|
-
richTextStarterKitOptions,
|
|
104
|
-
richTextToolbarItems
|
|
105
|
-
};
|
|
142
|
+
export { isSafeFileUrl, isSafeImageSrc, isSafeLinkUrl, richTextBlockTypes, richTextSanitizeOptions, richTextStarterKitOptions, richTextToolbarItems };
|
|
@@ -5,10 +5,28 @@
|
|
|
5
5
|
* (`/posts?page=2#top`) so it works identically in a loader, in node, and in the
|
|
6
6
|
* browser. Adapters map their host location onto `CheloneLocation`.
|
|
7
7
|
*/
|
|
8
|
+
/**
|
|
9
|
+
* What Chelone writes into history state. An interface, so an app adds its own
|
|
10
|
+
* keys by augmenting `@chelone/core`.
|
|
11
|
+
*/
|
|
12
|
+
export interface LocationState {
|
|
13
|
+
/** `false` keeps the scroll position on navigation. */
|
|
14
|
+
_scrollToTop?: boolean;
|
|
15
|
+
/** Where to go once a blocked navigation is confirmed. */
|
|
16
|
+
nextPathname?: string;
|
|
17
|
+
/** A seed record of any resource, read by `getRecordFromLocation`. */
|
|
18
|
+
record?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Read browser history state, which is untyped input, as a `LocationState`.
|
|
22
|
+
* `null` for anything that is not an object or whose known keys have the wrong
|
|
23
|
+
* type. App keys pass through unchecked.
|
|
24
|
+
*/
|
|
25
|
+
export declare function toLocationState(raw: unknown): LocationState | null;
|
|
8
26
|
/**
|
|
9
27
|
* A framework-neutral location snapshot. `search` is the raw `?a=b` string
|
|
10
28
|
* (leading `?` included, `''` when empty); `searchParams` is the parsed
|
|
11
|
-
* convenience view; `state` is `
|
|
29
|
+
* convenience view; `state` is a `LocationState` or `null`.
|
|
12
30
|
* `key` is native in react-router, synthesized for TanStack (plan §0b-4).
|
|
13
31
|
*/
|
|
14
32
|
export interface CheloneLocation {
|
|
@@ -16,7 +34,7 @@ export interface CheloneLocation {
|
|
|
16
34
|
search: string;
|
|
17
35
|
searchParams: URLSearchParams;
|
|
18
36
|
hash: string;
|
|
19
|
-
state:
|
|
37
|
+
state: LocationState | null;
|
|
20
38
|
key?: string;
|
|
21
39
|
}
|
|
22
40
|
/**
|
|
@@ -26,7 +44,7 @@ export interface CheloneLocation {
|
|
|
26
44
|
*/
|
|
27
45
|
export interface NavigateOpts {
|
|
28
46
|
replace?: boolean;
|
|
29
|
-
state?:
|
|
47
|
+
state?: LocationState;
|
|
30
48
|
resetScroll?: boolean;
|
|
31
49
|
}
|
|
32
50
|
/**
|
|
@@ -34,4 +52,4 @@ export interface NavigateOpts {
|
|
|
34
52
|
* `state`/`key` are threaded through from the caller (a router adapter owns the
|
|
35
53
|
* history entry's state; a bare parse has none).
|
|
36
54
|
*/
|
|
37
|
-
export declare function parseLocation(to: string, state?:
|
|
55
|
+
export declare function parseLocation(to: string, state?: LocationState | null, key?: string): CheloneLocation;
|
package/dist/router/location.js
CHANGED
|
@@ -1,20 +1,43 @@
|
|
|
1
|
+
import { isRecord } from "../guards.js";
|
|
2
|
+
// src/router/location.ts
|
|
3
|
+
/**
|
|
4
|
+
* Framework-neutral location types + a pure, SSR-safe parser.
|
|
5
|
+
*
|
|
6
|
+
* No `window` at import (or ever): `parseLocation` takes a full path string
|
|
7
|
+
* (`/posts?page=2#top`) so it works identically in a loader, in node, and in the
|
|
8
|
+
* browser. Adapters map their host location onto `CheloneLocation`.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Read browser history state, which is untyped input, as a `LocationState`.
|
|
12
|
+
* `null` for anything that is not an object or whose known keys have the wrong
|
|
13
|
+
* type. App keys pass through unchecked.
|
|
14
|
+
*/
|
|
15
|
+
function toLocationState(raw) {
|
|
16
|
+
return isLocationState(raw) ? raw : null;
|
|
17
|
+
}
|
|
18
|
+
function isLocationState(raw) {
|
|
19
|
+
if (!isRecord(raw)) return false;
|
|
20
|
+
const { _scrollToTop, nextPathname, record } = raw;
|
|
21
|
+
return (_scrollToTop === void 0 || typeof _scrollToTop === "boolean") && (nextPathname === void 0 || typeof nextPathname === "string") && (record === void 0 || isRecord(record));
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Parse a full path string into a `CheloneLocation`. Pure — no `window`.
|
|
25
|
+
* `state`/`key` are threaded through from the caller (a router adapter owns the
|
|
26
|
+
* history entry's state; a bare parse has none).
|
|
27
|
+
*/
|
|
1
28
|
function parseLocation(to, state = null, key) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
key
|
|
16
|
-
};
|
|
29
|
+
const hashIndex = to.indexOf("#");
|
|
30
|
+
const hash = hashIndex >= 0 ? to.slice(hashIndex) : "";
|
|
31
|
+
const withoutHash = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
|
|
32
|
+
const searchIndex = withoutHash.indexOf("?");
|
|
33
|
+
const search = searchIndex >= 0 ? withoutHash.slice(searchIndex) : "";
|
|
34
|
+
return {
|
|
35
|
+
pathname: (searchIndex >= 0 ? withoutHash.slice(0, searchIndex) : withoutHash) || "/",
|
|
36
|
+
search,
|
|
37
|
+
searchParams: new URLSearchParams(search),
|
|
38
|
+
hash,
|
|
39
|
+
state,
|
|
40
|
+
key
|
|
41
|
+
};
|
|
17
42
|
}
|
|
18
|
-
export {
|
|
19
|
-
parseLocation
|
|
20
|
-
};
|
|
43
|
+
export { parseLocation, toLocationState };
|