@openleaf-editor/element 0.1.0-beta.2 → 0.1.0-beta.4

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/README.md CHANGED
@@ -5,6 +5,17 @@ The `<openleaf-editor>` custom element: OpenLeaf's drop-in for CMS forms. HTML i
5
5
  This is a **beta** (`0.1.0-beta.2`). APIs may still change. It has not been
6
6
  used in production, and it has not been driven by a real screen reader.
7
7
 
8
+ > **Editor output is untrusted input.** Whatever the editor produces — and
9
+ > whatever a user pasted into it — must be sanitized **on your server** before it
10
+ > is stored or rendered as HTML. Client-side sanitization is a user-experience
11
+ > feature, not a security control: anything the editor strips can be put back
12
+ > with developer tools, because the editor runs under the user's control.
13
+ >
14
+ > [`@openleaf-editor/sanitize`](https://github.com/PeytonNowlin/openleaf/tree/main/packages/sanitize) ships the
15
+ > canonical allowlist as data and generates configuration for DOMPurify, Python
16
+ > `bleach` and PHP HTMLPurifier from it, so client and server enforce the same
17
+ > rules. Read [SECURITY.md](https://github.com/PeytonNowlin/openleaf/blob/main/SECURITY.md) before you ship.
18
+
8
19
  ## Install
9
20
 
10
21
  ```bash
@@ -85,8 +96,47 @@ registerImageUploader(async (file) => {
85
96
 
86
97
  Whatever that function throws is shown to the author verbatim, so write the
87
98
  message for them. For one editor with its own endpoint, set
88
- `element.imageUploader` instead.
99
+ `element.imageUploader` instead. JPEG (including `.jfif`), PNG, GIF, WebP and
100
+ AVIF are accepted; HEIC/HEIF is refused with a message rather than converted.
101
+
102
+ ## Sanitizing submitted HTML
103
+
104
+ Sanitize on the server — see the note at the top of this file for why.
105
+ `@openleaf-editor/sanitize` ships the same allowlist as data, including the
106
+ narrow `style` allowance that alignment and colour need.
89
107
 
90
- Sanitize submitted HTML on the server. `@openleaf-editor/sanitize` ships the same allowlist as data — including the narrow `style` allowance that alignment and colour need. If you sanitize with DOMPurify, install `styleAttributeHook` as well; the config alone cannot filter CSS per element.
108
+ With DOMPurify, this is the whole setup:
109
+
110
+ ```js
111
+ import DOMPurify from 'dompurify'
112
+ import { configureDOMPurify, DEFAULT_POLICY } from '@openleaf-editor/sanitize'
113
+
114
+ const purify = DOMPurify(window)
115
+ const config = configureDOMPurify(purify, DEFAULT_POLICY)
116
+ const clean = purify.sanitize(dirty, config)
117
+ ```
91
118
 
92
- See the [project README](https://github.com/PeytonNowlin/openleaf) for the rest.
119
+ One call, because the safe setup has to be atomic. `configureDOMPurify` installs
120
+ **both** hooks the policy needs and then returns a config that enables the
121
+ features they guard:
122
+
123
+ - `styleAttributeHook`, because `ALLOWED_ATTR` is global and DOMPurify filters no
124
+ CSS properties of its own — without it, permitting `style` permits
125
+ `position:fixed;inset:0`, a page-covering overlay that looks like your own UI.
126
+ - `embedHook`, because the policy allows an `<iframe>` only when its `src` is on a
127
+ closed list of player hosts, which no DOMPurify config can express. Enabling
128
+ iframes without it would let `<iframe src="https://evil.example">` through the
129
+ sanitizer this file recommends.
130
+
131
+ Reach for the individual hooks or `toDOMPurifyConfig` only if you manage
132
+ DOMPurify hooks yourself. Both fail closed — they drop styles and iframes rather
133
+ than trusting them — so the failure mode of getting it wrong is content loss
134
+ rather than XSS. That is the right direction, and it is still worth not doing.
135
+
136
+ ## More
137
+
138
+ - [API reference](https://github.com/PeytonNowlin/openleaf/blob/main/docs/api-reference.md) —
139
+ every attribute, property and `openleaf:*` event.
140
+ - [SECURITY.md](https://github.com/PeytonNowlin/openleaf/blob/main/SECURITY.md) —
141
+ the threat model and a baseline CSP.
142
+ - [Project README](https://github.com/PeytonNowlin/openleaf) for the rest.
@@ -0,0 +1,46 @@
1
+ /** Owns the custom element's textarea and form-submission contract. */
2
+ export declare class FormBridge {
3
+ #private;
4
+ private readonly host;
5
+ private readonly readValue;
6
+ private readonly writeValue;
7
+ constructor(host: HTMLElement, readValue: () => string, writeValue: (html: string) => void);
8
+ get textarea(): HTMLTextAreaElement | null;
9
+ bind(): HTMLTextAreaElement | null;
10
+ attach(): void;
11
+ detach(): void;
12
+ rebind(): void;
13
+ /**
14
+ * Write the document into the textarea.
15
+ *
16
+ * `value` is an optional already-serialized copy. The keystroke path has one
17
+ * in hand -- it is about to put the same string in the change event's detail
18
+ * -- and serializing the document twice per keystroke was measurable on a
19
+ * large post.
20
+ *
21
+ * An explicit sync also cancels a pending debounced one and clears the dirty
22
+ * flag: the write it was going to make has just happened.
23
+ */
24
+ sync(value?: string): void;
25
+ /**
26
+ * Record that the document changed, without serializing it.
27
+ *
28
+ * The serialization is what costs -- a `DOMSerializer` pass over the whole
29
+ * document and a ~0.33 MB string per keystroke on a 100-page document -- and
30
+ * nothing reads the result until one of the flush points below. So the change
31
+ * is only noted here, and the trailing timer exists for hosts that watch the
32
+ * textarea rather than the element.
33
+ */
34
+ markDirty(): void;
35
+ /**
36
+ * Write the document to the textarea if, and only if, it would differ.
37
+ *
38
+ * Two ways it can: the document changed since the last write, or something
39
+ * outside wrote to the textarea itself -- a script, a server re-render, a
40
+ * test. The second is why this cannot be a bare dirty check. Comparing
41
+ * against the string last written costs a string compare and no
42
+ * serialization, so the flush points stay free when nothing has moved.
43
+ */
44
+ flush(): void;
45
+ }
46
+ //# sourceMappingURL=form-bridge.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-bridge.d.ts","sourceRoot":"","sources":["../src/form-bridge.ts"],"names":[],"mappings":"AAYA,uEAAuE;AACvE,qBAAa,UAAU;;IASnB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,UAAU;gBAFV,IAAI,EAAE,WAAW,EACjB,SAAS,EAAE,MAAM,MAAM,EACvB,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;IAGrD,IAAI,QAAQ,IAAI,mBAAmB,GAAG,IAAI,CAEzC;IAED,IAAI,IAAI,mBAAmB,GAAG,IAAI;IAmBlC,MAAM,IAAI,IAAI;IAmBd,MAAM,IAAI,IAAI;IAWd,MAAM,IAAI,IAAI;IAMd;;;;;;;;;;OAUG;IACH,IAAI,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAS1B;;;;;;;;OAQG;IACH,SAAS,IAAI,IAAI;IAUjB;;;;;;;;OAQG;IACH,KAAK,IAAI,IAAI;CA0Bd"}
@@ -0,0 +1,158 @@
1
+ /**
2
+ * How long a document change may sit unwritten before the textarea catches up.
3
+ *
4
+ * Nothing reads the textarea between edits -- `submit` and `formdata` both
5
+ * force a write first, and so does teardown -- so this exists only for a host
6
+ * that watches the textarea itself, with a MutationObserver or a polling
7
+ * autosave. Writing on every keystroke instead cost a full re-serialization of
8
+ * the document per character: 12 ms on a plain 100-page document, 74 ms with
9
+ * tables, against a 16.7 ms frame.
10
+ */
11
+ const SYNC_DELAY_MS = 300;
12
+ /** Owns the custom element's textarea and form-submission contract. */
13
+ export class FormBridge {
14
+ host;
15
+ readValue;
16
+ writeValue;
17
+ #textarea = null;
18
+ #form = null;
19
+ #dirty = false;
20
+ /** The last string this bridge wrote, so a foreign write is detectable. */
21
+ #written = null;
22
+ #timer = null;
23
+ constructor(host, readValue, writeValue) {
24
+ this.host = host;
25
+ this.readValue = readValue;
26
+ this.writeValue = writeValue;
27
+ }
28
+ get textarea() {
29
+ return this.#textarea;
30
+ }
31
+ bind() {
32
+ const id = this.host.getAttribute('for');
33
+ if (id) {
34
+ const root = this.host.getRootNode();
35
+ const element = root.getElementById?.(id);
36
+ if (element instanceof HTMLTextAreaElement)
37
+ this.#textarea = element;
38
+ else {
39
+ this.#textarea = null;
40
+ console.error(`<openleaf-editor for="${id}">: no <textarea id="${id}"> found. ` +
41
+ 'Content will not be submitted with the form.');
42
+ }
43
+ return this.#textarea;
44
+ }
45
+ this.#textarea = this.host.querySelector('textarea');
46
+ return this.#textarea;
47
+ }
48
+ attach() {
49
+ this.detach();
50
+ // Re-resolve a `for=` binding that could not be resolved at build time. A
51
+ // wrapper builds the element while it is still detached, and a detached
52
+ // root has no `getElementById`, so `bind()` found nothing and every later
53
+ // write went nowhere -- the editor looked right and the textarea it posts
54
+ // still held the server's original HTML.
55
+ //
56
+ // Safe here in a way `rebind()` is not: this only re-runs for an explicit
57
+ // `for`, whose id lookup is unambiguous, where the nested
58
+ // `querySelector('textarea')` can match the source box instead.
59
+ if (!this.#textarea && this.host.getAttribute('for'))
60
+ this.bind();
61
+ this.#form = this.#textarea?.form ?? this.host.closest('form');
62
+ this.#form?.addEventListener('submit', this.#onSubmit);
63
+ this.#form?.addEventListener('formdata', this.#onFormData);
64
+ this.#form?.addEventListener('reset', this.#onReset);
65
+ this.sync();
66
+ }
67
+ detach() {
68
+ // Flush first. A host that removes the editor and then reads the textarea
69
+ // -- a framework unmounting a component, a wizard swapping a step -- must
70
+ // not get the value as of the last debounce tick.
71
+ this.flush();
72
+ this.#form?.removeEventListener('submit', this.#onSubmit);
73
+ this.#form?.removeEventListener('formdata', this.#onFormData);
74
+ this.#form?.removeEventListener('reset', this.#onReset);
75
+ this.#form = null;
76
+ }
77
+ rebind() {
78
+ this.detach();
79
+ this.bind();
80
+ this.attach();
81
+ }
82
+ /**
83
+ * Write the document into the textarea.
84
+ *
85
+ * `value` is an optional already-serialized copy. The keystroke path has one
86
+ * in hand -- it is about to put the same string in the change event's detail
87
+ * -- and serializing the document twice per keystroke was measurable on a
88
+ * large post.
89
+ *
90
+ * An explicit sync also cancels a pending debounced one and clears the dirty
91
+ * flag: the write it was going to make has just happened.
92
+ */
93
+ sync(value) {
94
+ this.#cancel();
95
+ this.#dirty = false;
96
+ if (!this.#textarea)
97
+ return;
98
+ const html = value ?? this.readValue();
99
+ this.#textarea.value = html;
100
+ this.#written = html;
101
+ }
102
+ /**
103
+ * Record that the document changed, without serializing it.
104
+ *
105
+ * The serialization is what costs -- a `DOMSerializer` pass over the whole
106
+ * document and a ~0.33 MB string per keystroke on a 100-page document -- and
107
+ * nothing reads the result until one of the flush points below. So the change
108
+ * is only noted here, and the trailing timer exists for hosts that watch the
109
+ * textarea rather than the element.
110
+ */
111
+ markDirty() {
112
+ if (!this.#textarea)
113
+ return;
114
+ this.#dirty = true;
115
+ if (this.#timer !== null)
116
+ return;
117
+ this.#timer = setTimeout(() => {
118
+ this.#timer = null;
119
+ this.flush();
120
+ }, SYNC_DELAY_MS);
121
+ }
122
+ /**
123
+ * Write the document to the textarea if, and only if, it would differ.
124
+ *
125
+ * Two ways it can: the document changed since the last write, or something
126
+ * outside wrote to the textarea itself -- a script, a server re-render, a
127
+ * test. The second is why this cannot be a bare dirty check. Comparing
128
+ * against the string last written costs a string compare and no
129
+ * serialization, so the flush points stay free when nothing has moved.
130
+ */
131
+ flush() {
132
+ const foreign = this.#textarea !== null && this.#written !== null && this.#textarea.value !== this.#written;
133
+ if (this.#dirty || foreign)
134
+ this.sync();
135
+ else
136
+ this.#cancel();
137
+ }
138
+ #cancel() {
139
+ if (this.#timer === null)
140
+ return;
141
+ clearTimeout(this.#timer);
142
+ this.#timer = null;
143
+ }
144
+ #onSubmit = () => this.flush();
145
+ #onFormData = (event) => {
146
+ this.flush();
147
+ if (this.#textarea?.name)
148
+ event.formData.set(this.#textarea.name, this.#textarea.value);
149
+ };
150
+ #onReset = () => {
151
+ // Reset fires before controls restore their defaults.
152
+ queueMicrotask(() => {
153
+ if (this.#textarea)
154
+ this.writeValue(this.#textarea.value);
155
+ });
156
+ };
157
+ }
158
+ //# sourceMappingURL=form-bridge.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form-bridge.js","sourceRoot":"","sources":["../src/form-bridge.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -36,9 +36,15 @@
36
36
  * autoresize grow the canvas with the document
37
37
  * toolbar-overflow collapse overflowing groups into a More menu
38
38
  * readonly render but do not allow editing
39
+ * autolink `false` to stop URLs becoming links on space or Enter.
40
+ * Trailing prose punctuation is left outside the mark.
41
+ * visualaids `false` to hide the guides for invisible structure
39
42
  * aria-label accessible name for the editable region
43
+ *
44
+ * See `docs/api-reference.md` for the properties, methods and events too.
40
45
  */
41
- import { Toolbar } from '@openleaf-editor/ui';
46
+ import { type ToolbarHandle, type ImageUploader } from '@openleaf-editor/ui';
47
+ import type { Schema } from 'prosemirror-model';
42
48
  import { EditorView } from 'prosemirror-view';
43
49
  /**
44
50
  * Emitted when the HTML source view opens and closes, carrying the textarea.
@@ -47,10 +53,23 @@ import { EditorView } from 'prosemirror-view';
47
53
  * formatting, syntax highlighting -- without the element having to know anything
48
54
  * about it. Names are defined here rather than imported so the element keeps no
49
55
  * dependency on any plugin.
56
+ *
57
+ * These fire on a REAL teardown, not on a DOM move. Moving the element keeps
58
+ * the whole session -- including source mode and the same textarea node -- so
59
+ * an enhancer that attached on open stays correctly attached, and gets its
60
+ * close only when the element is actually removed for good.
50
61
  */
51
62
  export declare const SOURCE_OPEN_EVENT = "openleaf:source-open";
52
63
  export declare const SOURCE_CLOSE_EVENT = "openleaf:source-close";
53
- export declare class OpenLeafEditor extends HTMLElement {
64
+ declare const HTMLElementBase: typeof HTMLElement;
65
+ export interface OpenLeafChangeDetail {
66
+ /** The document as HTML, already serialized. */
67
+ value: string;
68
+ }
69
+ export interface OpenLeafSourceDetail {
70
+ textarea: HTMLTextAreaElement;
71
+ }
72
+ export declare class OpenLeafEditor extends HTMLElementBase {
54
73
  #private;
55
74
  static get observedAttributes(): string[];
56
75
  /**
@@ -77,17 +96,61 @@ export declare class OpenLeafEditor extends HTMLElement {
77
96
  * nothing about this is visible to an author.
78
97
  */
79
98
  connectedCallback(): void;
99
+ /**
100
+ * Tear down -- but only once the element is really gone.
101
+ *
102
+ * Moving a node fires disconnect and then connect SYNCHRONOUSLY, so a guard
103
+ * in `connectedCallback` can never help: by the time it runs the view has
104
+ * already been destroyed. Deferring the decision by one microtask makes a
105
+ * move a no-op, which is what keeps undo history, selection and every
106
+ * plugin's state alive across a keyed-list reorder, an `insertBefore`
107
+ * shuffle or a drag-to-reorder.
108
+ *
109
+ * The limit is worth being precise about, because it is not "unmounting is
110
+ * safe now": this only covers a move completed within one task. Anything that
111
+ * parks the element in a detached container across ticks -- Vue's
112
+ * `<KeepAlive>` does exactly that -- is a real removal and tears down, which
113
+ * is why the rebuild path has to stay correct rather than merely unreachable.
114
+ */
80
115
  disconnectedCallback(): void;
81
116
  /** Current document as an HTML string. */
82
117
  get value(): string;
83
118
  set value(html: string);
119
+ /**
120
+ * Uploader for this editor alone, overriding `registerImageUploader`.
121
+ *
122
+ * An accessor rather than a class field, and that is load-bearing: a field
123
+ * initializer runs when the element upgrades, so it would overwrite an
124
+ * uploader assigned before the definition loaded -- exactly the case
125
+ * `#upgradeProperty` exists to rescue, defeated by the declaration meant to
126
+ * make the property visible.
127
+ */
128
+ get imageUploader(): ImageUploader | null;
129
+ set imageUploader(uploader: ImageUploader | null);
84
130
  /** Escape hatch for plugins and integrations that need the real view. */
85
131
  get view(): EditorView | null;
86
132
  /** The schema this editor was built with. */
87
- get schema(): import('prosemirror-model').Schema;
133
+ get schema(): Schema<string, string>;
88
134
  /** The toolbar, for plugins pushing external state via setItemState. */
89
- get toolbar(): Toolbar | null;
135
+ get toolbarInstance(): ToolbarHandle | null;
136
+ /** The `toolbar` attribute. Assigning reflects, as HTML properties should. */
137
+ get toolbar(): string | null;
138
+ set toolbar(layout: string | null);
139
+ /** The `toolbar2` attribute. */
140
+ get toolbar2(): string | null;
141
+ set toolbar2(layout: string | null);
142
+ /** The `menubar` attribute. */
143
+ get menubar(): string | null;
144
+ set menubar(menus: string | null);
145
+ /** The `formats` attribute. */
146
+ get formats(): string | null;
147
+ set formats(spec: string | null);
148
+ /** The `readonly` attribute, as the boolean every framework binds it as. */
149
+ get readOnly(): boolean;
150
+ set readOnly(value: boolean);
151
+ /** Whether the HTML source view is open. Assigning toggles it. */
90
152
  get sourceMode(): boolean;
153
+ set sourceMode(open: boolean);
91
154
  }
92
155
  /**
93
156
  * Re-exported so the single-file bundle can offer paste normalization without a
@@ -101,6 +164,52 @@ export { normalizePastedHtml } from '@openleaf-editor/paste';
101
164
  * `element.imageUploader` overrides it for one editor.
102
165
  */
103
166
  export { registerFilePicker, registerImageClasses, registerImageList, registerImageUploader, registerLinkList, registerTranslations, setUiLocale, type FilePicker, type FilePickerKind, type ImageUploadResult, type ImageUploader, type ListedResource, type PickedResource, } from '@openleaf-editor/ui';
167
+ /**
168
+ * Re-exported because it is the documented way to add a toolbar control and it
169
+ * lived in a package no install command mentions.
170
+ *
171
+ * `registerToolbarItem` is exported from `@openleaf-editor/ui`, which is a
172
+ * transitive dependency of this package and appears in no `npm install` line in
173
+ * any README. So the one extension point an integrator is most likely to reach
174
+ * for was reachable only by guessing at a package name. It is already in this
175
+ * bundle; re-exporting it costs nothing and means `registerToolbarItem` is
176
+ * available wherever `<openleaf-editor>` is -- including from
177
+ * `window.OpenLeaf` in a plain `<script>` integration, which has no other route
178
+ * to it at all.
179
+ *
180
+ * `t` comes with it: a custom control's label is translated by the toolbar, but
181
+ * anything the control builds itself has to be translated by the control.
182
+ */
183
+ export { registerIcons, registerStyles, registerToolbarItem, t, type ToolbarContext, type ToolbarControl, type ToolbarItemSpec, type ToolbarSelectOption, } from '@openleaf-editor/ui';
184
+ declare global {
185
+ interface HTMLElementTagNameMap {
186
+ 'openleaf-editor': OpenLeafEditor;
187
+ }
188
+ interface HTMLElementEventMap {
189
+ 'openleaf:change': CustomEvent<OpenLeafChangeDetail>;
190
+ 'openleaf:source-open': CustomEvent<OpenLeafSourceDetail>;
191
+ 'openleaf:source-close': CustomEvent<OpenLeafSourceDetail>;
192
+ }
193
+ /**
194
+ * The same three on `document` and `window`.
195
+ *
196
+ * `document.addEventListener` resolves against `DocumentEventMap`, not
197
+ * `HTMLElementEventMap`, so without these a delegated listener -- the ordinary
198
+ * way to watch every editor on a page, and the one `composed: true` now makes
199
+ * reliable from outside a shadow root -- was back to a bare `Event` and a cast
200
+ * to read `detail`.
201
+ */
202
+ interface DocumentEventMap {
203
+ 'openleaf:change': CustomEvent<OpenLeafChangeDetail>;
204
+ 'openleaf:source-open': CustomEvent<OpenLeafSourceDetail>;
205
+ 'openleaf:source-close': CustomEvent<OpenLeafSourceDetail>;
206
+ }
207
+ interface WindowEventMap {
208
+ 'openleaf:change': CustomEvent<OpenLeafChangeDetail>;
209
+ 'openleaf:source-open': CustomEvent<OpenLeafSourceDetail>;
210
+ 'openleaf:source-close': CustomEvent<OpenLeafSourceDetail>;
211
+ }
212
+ }
104
213
  /** Idempotent: safe to import twice, or alongside a bundled copy. */
105
214
  export declare function defineOpenLeafEditor(tag?: string): void;
106
215
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAkBH,OAAO,EAYL,OAAO,EAgBR,MAAM,qBAAqB,CAAA;AAK5B,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAI7C;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB,yBAAyB,CAAA;AACvD,eAAO,MAAM,kBAAkB,0BAA0B,CAAA;AAEzD,qBAAa,cAAe,SAAQ,WAAW;;IAC7C,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAED;;;;OAIG;IACH,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAuD5C;;;;;;;;;;;;;;;;OAgBG;IACH,iBAAiB,IAAI,IAAI;IAiPzB,oBAAoB,IAAI,IAAI;IAkC5B,0CAA0C;IAC1C,IAAI,KAAK,IAAI,MAAM,CAIlB;IAED,IAAI,KAAK,CAAC,IAAI,EAAE,MAAM,EAWrB;IAED,yEAAyE;IACzE,IAAI,IAAI,IAAI,UAAU,GAAG,IAAI,CAE5B;IAED,6CAA6C;IAC7C,IAAI,MAAM,IAAI,OAAO,mBAAmB,EAAE,MAAM,CAE/C;IAED,wEAAwE;IACxE,IAAI,OAAO,IAAI,OAAO,GAAG,IAAI,CAE5B;IAED,IAAI,UAAU,IAAI,OAAO,CAExB;CAmXF;AAED;;;;GAIG;AACH,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAA;AAE5D;;;;GAIG;AACH,OAAO,EACL,kBAAkB,EAClB,oBAAoB,EACpB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,oBAAoB,EACpB,WAAW,EACX,KAAK,UAAU,EACf,KAAK,cAAc,EACnB,KAAK,iBAAiB,EACtB,KAAK,aAAa,EAClB,KAAK,cAAc,EACnB,KAAK,cAAc,GACpB,MAAM,qBAAqB,CAAA;AAE5B,qEAAqE;AACrE,wBAAgB,oBAAoB,CAAC,GAAG,SAAoB,GAAG,IAAI,CAIlE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}