@danxbot/ui 7.6.0 → 7.7.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/dist/index.js +11610 -10923
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/types/components/MarkdownEditor.d.ts +28 -1
- package/dist/types/components/markdown/ImageUpload.d.ts +38 -0
- package/dist/types/icons/data.d.ts +29 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/lib/markdown/commands.d.ts +10 -4
- package/dist/types/lib/markdown/dom.d.ts +7 -22
- package/dist/types/lib/markdown/history.d.ts +12 -0
- package/dist/types/lib/markdown/hotkeys.d.ts +21 -2
- package/dist/types/lib/markdown/image-decoration.d.ts +60 -0
- package/dist/types/lib/markdown/image-pipeline.d.ts +153 -0
- package/dist/types/lib/markdown/images.d.ts +68 -0
- package/dist/types/lib/markdown/input.d.ts +19 -0
- package/dist/types/lib/markdown/reconcile.d.ts +6 -0
- package/dist/types/lib/markdown/surface.d.ts +63 -0
- package/package.json +1 -1
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { type LocalImageState } from "./image-decoration";
|
|
2
|
+
import { type SourceSelection } from "./position";
|
|
3
|
+
import type { MarkdownSurface } from "./surface";
|
|
4
|
+
/**
|
|
5
|
+
* Stores one image and says where it now lives.
|
|
6
|
+
*
|
|
7
|
+
* `signal` aborts when the editor unmounts; an uploader that ignores it still
|
|
8
|
+
* works, its result is simply discarded. The returned `url` must be one an
|
|
9
|
+
* `<img>` may load (`http(s)`, a relative URL, or `data:image/*` —
|
|
10
|
+
* `sanitizeImageSrc`) and never another `blob:`; anything else is reported
|
|
11
|
+
* as a failed upload rather than written into the document.
|
|
12
|
+
*/
|
|
13
|
+
export type ImageUploader = (file: File, options: {
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}) => Promise<{
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
/**
|
|
19
|
+
* What the consumer needs to decide whether it may save (PLN-10 R-1).
|
|
20
|
+
*
|
|
21
|
+
* Counts IMAGES IN THE CURRENT DOCUMENT, one per construct — the same unit
|
|
22
|
+
* `findUnstoredImages` returns, so `unstored === findUnstoredImages(value).length`
|
|
23
|
+
* always. `uploading` are being stored, `failed` cannot be stored without the
|
|
24
|
+
* author acting (a rejected upload, or a `blob:` this editor did not mint), and
|
|
25
|
+
* `unstored` is every image not stored yet — `uploading + failed` plus those
|
|
26
|
+
* with no storage configured. `unstored > 0` means the value must not be saved.
|
|
27
|
+
*/
|
|
28
|
+
export interface ImageUploadState {
|
|
29
|
+
uploading: number;
|
|
30
|
+
failed: number;
|
|
31
|
+
unstored: number;
|
|
32
|
+
}
|
|
33
|
+
/** The editor's own view: the public counts, plus what its chrome needs. */
|
|
34
|
+
export interface ImagePipelineSnapshot extends ImageUploadState {
|
|
35
|
+
/** Images that cannot be stored because there is no uploader. */
|
|
36
|
+
local: number;
|
|
37
|
+
/** Failed images this editor still holds the File for, so Retry can work. */
|
|
38
|
+
retryable: number;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The browser facilities the pipeline touches, injectable so a test never
|
|
42
|
+
* needs a real object URL or a real network.
|
|
43
|
+
*/
|
|
44
|
+
export interface ImagePipelineEnvironment {
|
|
45
|
+
createObjectURL: (file: File) => string;
|
|
46
|
+
revokeObjectURL: (url: string) => void;
|
|
47
|
+
/**
|
|
48
|
+
* Settles once `url` has loaded or failed to. The swap waits for it so the
|
|
49
|
+
* stored picture is already in the cache when it replaces the local one —
|
|
50
|
+
* otherwise the image blinks empty for as long as the download takes.
|
|
51
|
+
*/
|
|
52
|
+
preload: (url: string) => Promise<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* The longest the swap waits for `preload`. A stored URL whose download
|
|
55
|
+
* stalls would otherwise hold an image that IS stored in "uploading" for
|
|
56
|
+
* ever — save disabled, page-leave guarded — for want of a cache warm-up.
|
|
57
|
+
*/
|
|
58
|
+
preloadTimeoutMs: number;
|
|
59
|
+
}
|
|
60
|
+
export interface ImagePipelineOptions {
|
|
61
|
+
/** The uploader in force right now, or null when there is no storage. */
|
|
62
|
+
uploader: () => ImageUploader | null;
|
|
63
|
+
/** Called whenever the snapshot changes, and once when attached. */
|
|
64
|
+
onChange: (snapshot: ImagePipelineSnapshot) => void;
|
|
65
|
+
environment?: Partial<ImagePipelineEnvironment>;
|
|
66
|
+
}
|
|
67
|
+
export declare class ImagePipeline {
|
|
68
|
+
private readonly options;
|
|
69
|
+
private surface;
|
|
70
|
+
private readonly entries;
|
|
71
|
+
private readonly env;
|
|
72
|
+
private last;
|
|
73
|
+
private disposed;
|
|
74
|
+
constructor(options: ImagePipelineOptions);
|
|
75
|
+
/**
|
|
76
|
+
* The surface's `ImageResolver`: the state of an object URL this editor
|
|
77
|
+
* minted, or null for anything else. An arrow property so it can be handed
|
|
78
|
+
* to the surface before the surface exists.
|
|
79
|
+
*/
|
|
80
|
+
readonly resolve: (src: string) => LocalImageState | null;
|
|
81
|
+
/** Connects the surface this pipeline edits, and reports the first snapshot. */
|
|
82
|
+
attach(surface: MarkdownSurface): void;
|
|
83
|
+
/**
|
|
84
|
+
* Binds the overlay buttons' Retry and Remove. Returns the unbind function.
|
|
85
|
+
*
|
|
86
|
+
* ONE DELEGATED LISTENER on the root, because the buttons live inside
|
|
87
|
+
* decorations the reconciler rebuilds whenever an image's state changes — a
|
|
88
|
+
* listener on a button would be thrown away with it.
|
|
89
|
+
*/
|
|
90
|
+
bind(): () => void;
|
|
91
|
+
/**
|
|
92
|
+
* THE pipeline: every image file that enters the editor comes through here —
|
|
93
|
+
* paste, drop and the file picker alike.
|
|
94
|
+
*
|
|
95
|
+
* `at` is where the images go: the caret for a paste or a pick, the drop
|
|
96
|
+
* point for a drop. With none (an editor that was never focused), they go at
|
|
97
|
+
* the end of the document — never at offset 0 in front of whatever the
|
|
98
|
+
* author wrote first. Non-image files are ignored; the picker's `accept`
|
|
99
|
+
* is a hint the user can override, not a guarantee.
|
|
100
|
+
*
|
|
101
|
+
* Returns the object URLs it inserted, in order.
|
|
102
|
+
*/
|
|
103
|
+
insert(files: readonly File[], at: SourceSelection | null): string[];
|
|
104
|
+
/**
|
|
105
|
+
* Uploads a failed image again, with the SAME File it was inserted with.
|
|
106
|
+
*
|
|
107
|
+
* With no uploader configured any more, the image becomes local-only rather
|
|
108
|
+
* than staying "failed" — the honest state for something nothing can store.
|
|
109
|
+
*/
|
|
110
|
+
retry(src: string): void;
|
|
111
|
+
/** Retries every failed image still in the document. */
|
|
112
|
+
retryFailed(): void;
|
|
113
|
+
/**
|
|
114
|
+
* Starts uploading every local-only image once an uploader exists.
|
|
115
|
+
*
|
|
116
|
+
* The component calls this when its uploader changes: an image pasted
|
|
117
|
+
* before an app's storage provider mounted should not stay unsaveable when
|
|
118
|
+
* the storage it needed arrives a moment later.
|
|
119
|
+
*/
|
|
120
|
+
uploaderChanged(): void;
|
|
121
|
+
/**
|
|
122
|
+
* Recomputes the snapshot from the CURRENT document, and releases object
|
|
123
|
+
* URLs nothing can reach any more. Called after every change to the source.
|
|
124
|
+
*
|
|
125
|
+
* THE COUNTS COME FROM `findUnstoredImages`, the same exported function a
|
|
126
|
+
* consumer's save path calls — one detection source, never a second opinion
|
|
127
|
+
* the two could disagree on (DX-3134).
|
|
128
|
+
*
|
|
129
|
+
* AN OBJECT URL IS RELEASED WHEN ITS IMAGE IS GONE AND SETTLED: not in the
|
|
130
|
+
* document, not in any undo or redo entry (Ctrl+Z could bring it back), and
|
|
131
|
+
* not mid-upload (the upload will swap it wherever it still is). Revoking one
|
|
132
|
+
* any earlier turns an undo into a broken image; never revoking leaks the
|
|
133
|
+
* file's bytes for the life of the page.
|
|
134
|
+
*/
|
|
135
|
+
sync(): void;
|
|
136
|
+
/**
|
|
137
|
+
* Stops everything: aborts every upload in flight and releases every object
|
|
138
|
+
* URL. The editor is going away, so nothing it minted can be drawn anywhere
|
|
139
|
+
* again — no other editor will render another's `blob:`.
|
|
140
|
+
*/
|
|
141
|
+
dispose(): void;
|
|
142
|
+
private upload;
|
|
143
|
+
/**
|
|
144
|
+
* Resolves when `work` settles, `preloadTimeoutMs` passes, or `signal`
|
|
145
|
+
* aborts — never rejects. Every timer and listener is cleaned up.
|
|
146
|
+
*/
|
|
147
|
+
private settled;
|
|
148
|
+
private release;
|
|
149
|
+
/** An image's state moved: redraw its decoration and re-report. */
|
|
150
|
+
private changed;
|
|
151
|
+
private removeImage;
|
|
152
|
+
private requireSurface;
|
|
153
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/** An image whose picture exists only in this page's memory. */
|
|
2
|
+
export interface UnstoredImage {
|
|
3
|
+
/** The `blob:` URL exactly as the source spells it. */
|
|
4
|
+
src: string;
|
|
5
|
+
alt: string;
|
|
6
|
+
/** The whole `` construct, as source offsets. */
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}
|
|
10
|
+
/** Whether an image src names a picture that has not been stored anywhere. */
|
|
11
|
+
export declare function isUnstoredImageSrc(src: string): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Every image in `markdown` whose src is a `blob:` URL, in source order.
|
|
14
|
+
*
|
|
15
|
+
* THE SINGLE DETECTION SOURCE. The editor's own warning strip, its
|
|
16
|
+
* `onImageUploadStateChange` counts and its `beforeunload` guard all read this
|
|
17
|
+
* function, and it is exported so a consumer's save path can re-check the
|
|
18
|
+
* exact value it is about to persist with the same answer (DX-3134, PLN-10
|
|
19
|
+
* "Pending-state event").
|
|
20
|
+
*
|
|
21
|
+
* Read from the PARSER, not with a regex, for the reason `constructAt` gives:
|
|
22
|
+
* the parser has already decided what is an image, including the cases a
|
|
23
|
+
* pattern gets wrong — a `` inside a code span is prose, not a
|
|
24
|
+
* picture, and saving it loses nothing.
|
|
25
|
+
*/
|
|
26
|
+
export declare function findUnstoredImages(markdown: string): UnstoredImage[];
|
|
27
|
+
/**
|
|
28
|
+
* A URL written so the destination parses as exactly that URL.
|
|
29
|
+
*
|
|
30
|
+
* Whitespace ends a destination, an unbalanced parenthesis ends the construct,
|
|
31
|
+
* and a backslash before punctuation is an escape markdown would consume — so
|
|
32
|
+
* all of them are percent-encoded, which is the same URL to every server and
|
|
33
|
+
* the only spelling that survives being written into markdown.
|
|
34
|
+
*/
|
|
35
|
+
export declare function markdownDestination(url: string): string;
|
|
36
|
+
/** ``, with both halves escaped so they read back as themselves. */
|
|
37
|
+
export declare function imageMarkdown(src: string, alt: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* The alt text a file brings with it: its name without the extension.
|
|
40
|
+
*
|
|
41
|
+
* A pasted screenshot is called `image.png` by every browser, so the result is
|
|
42
|
+
* "image" — not a description, but an honest placeholder the author can see
|
|
43
|
+
* and replace, which is better than the empty alt that makes an image
|
|
44
|
+
* invisible to a screen reader.
|
|
45
|
+
*/
|
|
46
|
+
export declare function altFromFileName(name: string): string;
|
|
47
|
+
/** The result of replacing every occurrence of one string with another. */
|
|
48
|
+
export interface Substitution {
|
|
49
|
+
source: string;
|
|
50
|
+
/** Carries an offset in the old source to the same place in the new one. */
|
|
51
|
+
map: (offset: number) => number;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Replaces EVERY occurrence of `from` in `source` with `to`, or returns null
|
|
55
|
+
* when there is none.
|
|
56
|
+
*
|
|
57
|
+
* A PLAIN TEXT REPLACEMENT, deliberately, and only safe because of what `from`
|
|
58
|
+
* is: an object URL this editor minted, which embeds a random UUID and was
|
|
59
|
+
* never typed by anyone. Every occurrence of it is that image, whatever
|
|
60
|
+
* construct holds it now — including one the author has since turned into a
|
|
61
|
+
* link, which a parser-driven swap would leave pointing at a dead handle in
|
|
62
|
+
* the saved value (DX-3134).
|
|
63
|
+
*
|
|
64
|
+
* `map` is what lets the caret survive the swap: offsets before an occurrence
|
|
65
|
+
* stay, offsets after one shift by the length change, and an offset that was
|
|
66
|
+
* INSIDE a replaced URL lands at the end of its replacement.
|
|
67
|
+
*/
|
|
68
|
+
export declare function substituteAll(source: string, from: string, to: string): Substitution | null;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type SourceSelection } from "./position";
|
|
2
|
+
import { type EditorRequest } from "./hotkeys";
|
|
1
3
|
import type { MarkdownSurface } from "./surface";
|
|
2
4
|
export interface InputOptions {
|
|
3
5
|
/**
|
|
@@ -8,8 +10,25 @@ export interface InputOptions {
|
|
|
8
10
|
* broken, and this is how that gets noticed before a user reports it.
|
|
9
11
|
*/
|
|
10
12
|
onUnhandled?: (inputType: string) => void;
|
|
13
|
+
/**
|
|
14
|
+
* Receives the image files of a paste or a drop, and where they go (DX-3134).
|
|
15
|
+
*
|
|
16
|
+
* `at` is the caret for a paste and the drop point for a drop, already on a
|
|
17
|
+
* caret stop. Without this option files are not handled here at all, and a
|
|
18
|
+
* paste or drop behaves exactly as it always did.
|
|
19
|
+
*/
|
|
20
|
+
onImageFiles?: (files: File[], at: SourceSelection | null) => void;
|
|
21
|
+
/**
|
|
22
|
+
* Receives a hotkey that asks the EDITOR to do something rather than making
|
|
23
|
+
* an edit — the Insert-image chord opening a file picker (DX-3134). A
|
|
24
|
+
* request with no receiver lets the key fall through, like a command that
|
|
25
|
+
* declined.
|
|
26
|
+
*/
|
|
27
|
+
onRequest?: (request: EditorRequest) => void;
|
|
11
28
|
}
|
|
12
29
|
/**
|
|
13
30
|
* Binds the input layer to a surface. Returns the unbind function.
|
|
14
31
|
*/
|
|
15
32
|
export declare function attachInput(surface: MarkdownSurface, options?: InputOptions): () => void;
|
|
33
|
+
/** Marks the surface while image files are dragged over it, for the affordance. */
|
|
34
|
+
export declare const DROP_TARGET_ATTR = "data-md-drop-target";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { PNode } from "./project";
|
|
2
|
+
import type { ImageResolver } from "./image-decoration";
|
|
2
3
|
/**
|
|
3
4
|
* Thrown when a patch would have had to disturb the node the browser is
|
|
4
5
|
* composing into.
|
|
@@ -17,6 +18,11 @@ export interface PatchOptions {
|
|
|
17
18
|
* removed; a patch that would need to do any of those throws.
|
|
18
19
|
*/
|
|
19
20
|
protect?: Text | null;
|
|
21
|
+
/**
|
|
22
|
+
* Which image srcs the calling editor minted, and their state (DX-3134).
|
|
23
|
+
* Absent means "none" — every `blob:` is then refused, as it always was.
|
|
24
|
+
*/
|
|
25
|
+
images?: ImageResolver;
|
|
20
26
|
}
|
|
21
27
|
export interface PatchStats {
|
|
22
28
|
/** Text nodes whose `data` was assigned. */
|
|
@@ -2,6 +2,7 @@ import { type CaretMap } from "./caret-map";
|
|
|
2
2
|
import { type Projection, type ProjectOptions } from "./project";
|
|
3
3
|
import { type SourceSelection } from "./position";
|
|
4
4
|
import { type EditKind, type HistoryOptions } from "./history";
|
|
5
|
+
import type { ImageResolver } from "./image-decoration";
|
|
5
6
|
export interface SurfaceOptions {
|
|
6
7
|
/** Called after every committed change, with the new source. */
|
|
7
8
|
onChange?: (source: string) => void;
|
|
@@ -20,6 +21,12 @@ export interface SurfaceOptions {
|
|
|
20
21
|
* eats the user's text if it is swallowed.
|
|
21
22
|
*/
|
|
22
23
|
onDesync?: (detail: string) => void;
|
|
24
|
+
/**
|
|
25
|
+
* Which object URLs this surface's editor minted, and what state each is in
|
|
26
|
+
* (DX-3134). Consulted on every render, so a state change only needs
|
|
27
|
+
* `refresh()`. Absent: no `blob:` src ever renders.
|
|
28
|
+
*/
|
|
29
|
+
images?: ImageResolver;
|
|
23
30
|
}
|
|
24
31
|
export interface Splice {
|
|
25
32
|
start: number;
|
|
@@ -30,6 +37,17 @@ export declare class MarkdownSurface {
|
|
|
30
37
|
readonly root: HTMLElement;
|
|
31
38
|
private source;
|
|
32
39
|
private readonly options;
|
|
40
|
+
/**
|
|
41
|
+
* Upload swaps that arrived mid-composition, applied at `compositionend`.
|
|
42
|
+
*
|
|
43
|
+
* DX-3134: the composing text node carries the source range it came from,
|
|
44
|
+
* and `reconcileComposed` writes the composed text back into exactly that
|
|
45
|
+
* range. A swap applied mid-composition changes the length of the source
|
|
46
|
+
* before the node — the range would then point at the wrong characters, and
|
|
47
|
+
* the composed text would land inside a URL. So the swap waits, and runs
|
|
48
|
+
* right after the composition has been read back.
|
|
49
|
+
*/
|
|
50
|
+
private pendingSubstitutions;
|
|
33
51
|
/** The text node the browser is composing into, if any. */
|
|
34
52
|
private composing;
|
|
35
53
|
/** A projection that arrived while composing, applied at `compositionend`. */
|
|
@@ -82,6 +100,51 @@ export declare class MarkdownSurface {
|
|
|
82
100
|
* exactly right or exactly wrong, with nothing in between.
|
|
83
101
|
*/
|
|
84
102
|
apply(splice: Splice, select?: SourceSelection, kind?: EditKind): void;
|
|
103
|
+
/**
|
|
104
|
+
* Replaces every occurrence of `from` with `to` — in the document AND in
|
|
105
|
+
* every undo and redo entry — without recording an edit.
|
|
106
|
+
*
|
|
107
|
+
* THIS IS THE UPLOAD SWAP (DX-3134), and it is deliberately not an edit.
|
|
108
|
+
* When an image finishes uploading, its object URL is replaced by the stored
|
|
109
|
+
* URL. Recorded as an ordinary edit, Ctrl+Z would hand the object URL back —
|
|
110
|
+
* and a document holding it is a document that cannot be saved, for an image
|
|
111
|
+
* that WAS saved. Rewriting the history too means no undo, redo or later
|
|
112
|
+
* edit can resurrect a URL that no longer means anything.
|
|
113
|
+
*
|
|
114
|
+
* The caret stays where it was in the text the user can see: it is mapped
|
|
115
|
+
* through the substitution rather than moved, and only written back if it was
|
|
116
|
+
* in this surface to begin with — an upload finishing must never pull focus
|
|
117
|
+
* out of whatever the user moved on to. `onChange` fires, because the value
|
|
118
|
+
* genuinely changed and the consumer is the one who saves it.
|
|
119
|
+
*
|
|
120
|
+
* WHILE COMPOSING, the swap is queued and applied at `compositionend` — see
|
|
121
|
+
* `pendingSubstitutions`. A caller that needs to know when it has landed
|
|
122
|
+
* watches for `from` to leave the source (`onChange` fires when it does).
|
|
123
|
+
*/
|
|
124
|
+
substitute(from: string, to: string): void;
|
|
125
|
+
/**
|
|
126
|
+
* The selection, but only while this surface HAS FOCUS.
|
|
127
|
+
*
|
|
128
|
+
* DX-3134: a render that is not the user's own edit — an upload finishing, an
|
|
129
|
+
* image's state moving — writes the caret back only when the author is
|
|
130
|
+
* actually in this document. The document selection can still sit inside the
|
|
131
|
+
* surface after focus has gone to a button or another field, and writing a
|
|
132
|
+
* selection into an editing host is one of the things that pulls focus back
|
|
133
|
+
* into it: an upload landing would then yank the author out of whatever they
|
|
134
|
+
* moved on to.
|
|
135
|
+
*/
|
|
136
|
+
private focusedSelection;
|
|
137
|
+
/** Whether any undo or redo entry still holds `text` — see `History.includes`. */
|
|
138
|
+
remembers(text: string): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Re-renders the current document, keeping the caret.
|
|
141
|
+
*
|
|
142
|
+
* For a change the SOURCE does not show: an image's upload state lives in
|
|
143
|
+
* the resolver, so the decoration has to be redrawn when it moves (DX-3134).
|
|
144
|
+
* Unchanged text nodes are not touched, so the caret is only rewritten when
|
|
145
|
+
* this surface has focus — and then to the offsets it already had.
|
|
146
|
+
*/
|
|
147
|
+
refresh(): void;
|
|
85
148
|
/** Replaces the whole document, e.g. when the consumer's value prop changes. */
|
|
86
149
|
setSource(source: string): void;
|
|
87
150
|
get canUndo(): boolean;
|