@aerogel/core 0.0.0-next.6c539d8e63b397d4bb6c3d61a7f20a4d108b1cdd → 0.0.0-next.7035064d9ec6a82a936ee8dfcc4b58ed2e25a399
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/aerogel-core.cjs.js +1 -1
- package/dist/aerogel-core.cjs.js.map +1 -1
- package/dist/aerogel-core.d.ts +328 -72
- package/dist/aerogel-core.esm.js +1 -1
- package/dist/aerogel-core.esm.js.map +1 -1
- package/package.json +2 -2
- package/src/bootstrap/index.ts +12 -2
- package/src/components/headless/modals/AGHeadlessModal.ts +3 -1
- package/src/components/headless/modals/AGHeadlessModal.vue +10 -4
- package/src/components/headless/modals/AGHeadlessModalPanel.vue +10 -6
- package/src/components/headless/modals/AGHeadlessModalTitle.vue +14 -4
- package/src/components/lib/AGMarkdown.vue +14 -1
- package/src/components/lib/AGProgressBar.vue +30 -0
- package/src/components/lib/index.ts +1 -0
- package/src/components/modals/AGAlertModal.ts +5 -2
- package/src/components/modals/AGConfirmModal.ts +13 -5
- package/src/components/modals/AGConfirmModal.vue +1 -1
- package/src/components/modals/AGErrorReportModal.ts +5 -2
- package/src/components/modals/AGLoadingModal.ts +10 -4
- package/src/components/modals/AGModal.ts +1 -0
- package/src/components/modals/AGModalContext.vue +14 -4
- package/src/components/modals/AGPromptModal.ts +9 -4
- package/src/errors/JobCancelledError.ts +3 -0
- package/src/errors/utils.ts +16 -0
- package/src/forms/Form.ts +10 -3
- package/src/forms/index.ts +2 -1
- package/src/forms/utils.ts +20 -4
- package/src/forms/validation.ts +19 -0
- package/src/jobs/Job.ts +144 -2
- package/src/jobs/index.ts +4 -1
- package/src/jobs/listeners.ts +3 -0
- package/src/jobs/status.ts +4 -0
- package/src/services/App.state.ts +9 -1
- package/src/services/App.ts +5 -0
- package/src/services/Events.ts +13 -3
- package/src/services/Service.ts +107 -44
- package/src/services/Storage.ts +20 -0
- package/src/services/index.ts +7 -2
- package/src/services/utils.ts +18 -0
- package/src/testing/setup.ts +11 -3
- package/src/ui/UI.ts +108 -38
- package/src/utils/composition/persistent.test.ts +33 -0
- package/src/utils/composition/persistent.ts +11 -0
- package/src/utils/composition/state.test.ts +47 -0
- package/src/utils/composition/state.ts +24 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/markdown.test.ts +50 -0
- package/src/utils/markdown.ts +17 -2
- package/src/utils/vue.ts +4 -1
package/src/ui/UI.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { after, facade, fail, uuid } from '@noeldemartin/utils';
|
|
1
|
+
import { after, facade, fail, required, uuid } from '@noeldemartin/utils';
|
|
2
2
|
import { markRaw, nextTick } from 'vue';
|
|
3
3
|
import type { Component } from 'vue';
|
|
4
4
|
import type { ObjectValues } from '@noeldemartin/utils';
|
|
5
5
|
|
|
6
|
+
import App from '@/services/App';
|
|
6
7
|
import Events from '@/services/Events';
|
|
8
|
+
import type { AcceptRefs } from '@/utils';
|
|
7
9
|
import type { Color } from '@/components/constants';
|
|
8
10
|
import type { SnackbarAction, SnackbarColor } from '@/components/headless/snackbars';
|
|
9
11
|
import type { AGAlertModalProps, AGConfirmModalProps, AGLoadingModalProps, AGPromptModalProps } from '@/components';
|
|
@@ -34,14 +36,27 @@ export const UIComponents = {
|
|
|
34
36
|
|
|
35
37
|
export type UIComponent = ObjectValues<typeof UIComponents>;
|
|
36
38
|
|
|
37
|
-
export
|
|
39
|
+
export type ConfirmCheckboxes = Record<string, { label: string; default?: boolean; required?: boolean }>;
|
|
40
|
+
|
|
41
|
+
export type ConfirmOptions = AcceptRefs<{
|
|
38
42
|
acceptText?: string;
|
|
39
43
|
acceptColor?: Color;
|
|
40
44
|
cancelText?: string;
|
|
41
45
|
cancelColor?: Color;
|
|
46
|
+
actions?: Record<string, () => unknown>;
|
|
47
|
+
}>;
|
|
48
|
+
|
|
49
|
+
export type LoadingOptions = AcceptRefs<{
|
|
50
|
+
title?: string;
|
|
51
|
+
message?: string;
|
|
52
|
+
progress?: number;
|
|
53
|
+
}>;
|
|
54
|
+
|
|
55
|
+
export interface ConfirmOptionsWithCheckboxes<T extends ConfirmCheckboxes = ConfirmCheckboxes> extends ConfirmOptions {
|
|
56
|
+
checkboxes?: T;
|
|
42
57
|
}
|
|
43
58
|
|
|
44
|
-
export
|
|
59
|
+
export type PromptOptions = AcceptRefs<{
|
|
45
60
|
label?: string;
|
|
46
61
|
defaultValue?: string;
|
|
47
62
|
placeholder?: string;
|
|
@@ -50,7 +65,7 @@ export interface PromptOptions {
|
|
|
50
65
|
cancelText?: string;
|
|
51
66
|
cancelColor?: Color;
|
|
52
67
|
trim?: boolean;
|
|
53
|
-
}
|
|
68
|
+
}>;
|
|
54
69
|
|
|
55
70
|
export interface ShowSnackbarOptions {
|
|
56
71
|
component?: Component;
|
|
@@ -84,13 +99,18 @@ export class UIService extends Service {
|
|
|
84
99
|
this.openModal(this.requireComponent(UIComponents.AlertModal), getProperties());
|
|
85
100
|
}
|
|
86
101
|
|
|
102
|
+
/* eslint-disable max-len */
|
|
87
103
|
public async confirm(message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
88
104
|
public async confirm(title: string, message: string, options?: ConfirmOptions): Promise<boolean>;
|
|
105
|
+
public async confirm<T extends ConfirmCheckboxes>(message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
106
|
+
public async confirm<T extends ConfirmCheckboxes>(title: string, message: string, options?: ConfirmOptionsWithCheckboxes<T>): Promise<[boolean, Record<keyof T, boolean>]>; // prettier-ignore
|
|
107
|
+
/* eslint-enable max-len */
|
|
108
|
+
|
|
89
109
|
public async confirm(
|
|
90
110
|
messageOrTitle: string,
|
|
91
|
-
messageOrOptions?: string | ConfirmOptions,
|
|
92
|
-
options?: ConfirmOptions,
|
|
93
|
-
): Promise<boolean> {
|
|
111
|
+
messageOrOptions?: string | ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
112
|
+
options?: ConfirmOptions | ConfirmOptionsWithCheckboxes,
|
|
113
|
+
): Promise<boolean | [boolean, Record<string, boolean>]> {
|
|
94
114
|
const getProperties = (): AGConfirmModalProps => {
|
|
95
115
|
if (typeof messageOrOptions !== 'string') {
|
|
96
116
|
return {
|
|
@@ -105,14 +125,38 @@ export class UIService extends Service {
|
|
|
105
125
|
...(options ?? {}),
|
|
106
126
|
};
|
|
107
127
|
};
|
|
108
|
-
|
|
109
|
-
const modal = await this.openModal<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
128
|
+
const properties = getProperties();
|
|
129
|
+
const modal = await this.openModal<
|
|
130
|
+
ModalComponent<AGConfirmModalProps, boolean | [boolean, Record<string, boolean>]>
|
|
131
|
+
>(this.requireComponent(UIComponents.ConfirmModal), properties);
|
|
113
132
|
const result = await modal.beforeClose;
|
|
114
133
|
|
|
115
|
-
|
|
134
|
+
const confirmed = typeof result === 'object' ? result[0] : result ?? false;
|
|
135
|
+
const checkboxes =
|
|
136
|
+
typeof result === 'object'
|
|
137
|
+
? result[1]
|
|
138
|
+
: Object.entries(properties.checkboxes ?? {}).reduce(
|
|
139
|
+
(values, [checkbox, { default: defaultValue }]) => ({
|
|
140
|
+
[checkbox]: defaultValue ?? false,
|
|
141
|
+
...values,
|
|
142
|
+
}),
|
|
143
|
+
{} as Record<string, boolean>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
for (const [name, checkbox] of Object.entries(properties.checkboxes ?? {})) {
|
|
147
|
+
if (!checkbox.required || checkboxes[name]) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (confirmed && App.development) {
|
|
152
|
+
// eslint-disable-next-line no-console
|
|
153
|
+
console.warn(`Confirmed confirm modal was suppressed because required '${name}' checkbox was missing`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return [false, checkboxes];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return 'checkboxes' in properties ? [confirmed, checkboxes] : confirmed;
|
|
116
160
|
}
|
|
117
161
|
|
|
118
162
|
public async prompt(message: string, options?: PromptOptions): Promise<string | null>;
|
|
@@ -128,14 +172,14 @@ export class UIService extends Service {
|
|
|
128
172
|
return {
|
|
129
173
|
message: messageOrTitle,
|
|
130
174
|
...(messageOrOptions ?? {}),
|
|
131
|
-
};
|
|
175
|
+
} as AGPromptModalProps;
|
|
132
176
|
}
|
|
133
177
|
|
|
134
178
|
return {
|
|
135
179
|
title: messageOrTitle,
|
|
136
180
|
message: messageOrOptions,
|
|
137
181
|
...(options ?? {}),
|
|
138
|
-
};
|
|
182
|
+
} as AGPromptModalProps;
|
|
139
183
|
};
|
|
140
184
|
|
|
141
185
|
const modal = await this.openModal<ModalComponent<AGPromptModalProps, string | null>>(
|
|
@@ -150,25 +194,35 @@ export class UIService extends Service {
|
|
|
150
194
|
|
|
151
195
|
public async loading<T>(operation: Promise<T> | (() => T)): Promise<T>;
|
|
152
196
|
public async loading<T>(message: string, operation: Promise<T> | (() => T)): Promise<T>;
|
|
197
|
+
public async loading<T>(options: LoadingOptions, operation: Promise<T> | (() => T)): Promise<T>;
|
|
153
198
|
public async loading<T>(
|
|
154
|
-
|
|
199
|
+
operationOrMessageOrOptions: string | LoadingOptions | Promise<T> | (() => T),
|
|
155
200
|
operation?: Promise<T> | (() => T),
|
|
156
201
|
): Promise<T> {
|
|
157
|
-
const
|
|
158
|
-
|
|
159
|
-
|
|
202
|
+
const processOperation = (o: Promise<T> | (() => T)) => (typeof o === 'function' ? Promise.resolve(o()) : o);
|
|
203
|
+
const processArgs = (): { operationPromise: Promise<T>; props?: AGLoadingModalProps } => {
|
|
204
|
+
if (typeof operationOrMessageOrOptions === 'string') {
|
|
205
|
+
return {
|
|
206
|
+
props: { message: operationOrMessageOrOptions },
|
|
207
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (typeof operationOrMessageOrOptions === 'function' || operationOrMessageOrOptions instanceof Promise) {
|
|
212
|
+
return { operationPromise: processOperation(operationOrMessageOrOptions) };
|
|
160
213
|
}
|
|
161
214
|
|
|
162
|
-
return {
|
|
215
|
+
return {
|
|
216
|
+
props: operationOrMessageOrOptions,
|
|
217
|
+
operationPromise: processOperation(operation as Promise<T> | (() => T)),
|
|
218
|
+
};
|
|
163
219
|
};
|
|
164
220
|
|
|
165
|
-
const
|
|
221
|
+
const { operationPromise, props } = processArgs();
|
|
222
|
+
const modal = await this.openModal(this.requireComponent(UIComponents.LoadingModal), props);
|
|
166
223
|
|
|
167
224
|
try {
|
|
168
|
-
|
|
169
|
-
operation = typeof operation === 'function' ? Promise.resolve(operation()) : operation;
|
|
170
|
-
|
|
171
|
-
const [result] = await Promise.all([operation, after({ seconds: 1 })]);
|
|
225
|
+
const [result] = await Promise.all([operationPromise, after({ seconds: 1 })]);
|
|
172
226
|
|
|
173
227
|
return result;
|
|
174
228
|
} finally {
|
|
@@ -230,15 +284,42 @@ export class UIService extends Service {
|
|
|
230
284
|
}
|
|
231
285
|
|
|
232
286
|
public async closeModal(id: string, result?: unknown): Promise<void> {
|
|
287
|
+
if (!App.isMounted()) {
|
|
288
|
+
await this.removeModal(id, result);
|
|
289
|
+
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
233
293
|
await Events.emit('close-modal', { id, result });
|
|
234
294
|
}
|
|
235
295
|
|
|
296
|
+
public async closeAllModals(): Promise<void> {
|
|
297
|
+
while (this.modals.length > 0) {
|
|
298
|
+
await this.closeModal(required(this.modals[this.modals.length - 1]).id);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
236
302
|
protected async boot(): Promise<void> {
|
|
237
303
|
this.watchModalEvents();
|
|
238
304
|
this.watchMountedEvent();
|
|
239
305
|
this.watchViewportBreakpoints();
|
|
240
306
|
}
|
|
241
307
|
|
|
308
|
+
private async removeModal(id: string, result?: unknown): Promise<void> {
|
|
309
|
+
this.setState(
|
|
310
|
+
'modals',
|
|
311
|
+
this.modals.filter((m) => m.id !== id),
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
this.modalCallbacks[id]?.closed?.(result);
|
|
315
|
+
|
|
316
|
+
delete this.modalCallbacks[id];
|
|
317
|
+
|
|
318
|
+
const activeModal = this.modals.at(-1);
|
|
319
|
+
|
|
320
|
+
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
321
|
+
}
|
|
322
|
+
|
|
242
323
|
private watchModalEvents(): void {
|
|
243
324
|
Events.on('modal-will-close', ({ modal, result }) => {
|
|
244
325
|
this.modalCallbacks[modal.id]?.willClose?.(result);
|
|
@@ -248,19 +329,8 @@ export class UIService extends Service {
|
|
|
248
329
|
}
|
|
249
330
|
});
|
|
250
331
|
|
|
251
|
-
Events.on('modal-closed', async ({ modal, result }) => {
|
|
252
|
-
this.
|
|
253
|
-
'modals',
|
|
254
|
-
this.modals.filter((m) => m.id !== modal.id),
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
this.modalCallbacks[modal.id]?.closed?.(result);
|
|
258
|
-
|
|
259
|
-
delete this.modalCallbacks[modal.id];
|
|
260
|
-
|
|
261
|
-
const activeModal = this.modals.at(-1);
|
|
262
|
-
|
|
263
|
-
await (activeModal && Events.emit('show-modal', { id: activeModal.id }));
|
|
332
|
+
Events.on('modal-closed', async ({ modal: { id }, result }) => {
|
|
333
|
+
await this.removeModal(id, result);
|
|
264
334
|
});
|
|
265
335
|
}
|
|
266
336
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { nextTick } from 'vue';
|
|
3
|
+
import { Storage } from '@noeldemartin/utils';
|
|
4
|
+
|
|
5
|
+
import { persistent } from './persistent';
|
|
6
|
+
|
|
7
|
+
describe('Vue persistent helper', () => {
|
|
8
|
+
|
|
9
|
+
it('serializes to localStorage', async () => {
|
|
10
|
+
// Arrange
|
|
11
|
+
const store = persistent<{ foo?: string }>('foobar', {});
|
|
12
|
+
|
|
13
|
+
// Act
|
|
14
|
+
store.foo = 'bar';
|
|
15
|
+
|
|
16
|
+
await nextTick();
|
|
17
|
+
|
|
18
|
+
// Assert
|
|
19
|
+
expect(Storage.get('foobar')).toEqual({ foo: 'bar' });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('reads from localStorage', async () => {
|
|
23
|
+
// Arrange
|
|
24
|
+
Storage.set('foobar', { foo: 'bar' });
|
|
25
|
+
|
|
26
|
+
// Act
|
|
27
|
+
const store = persistent<{ foo?: string }>('foobar', {});
|
|
28
|
+
|
|
29
|
+
// Assert
|
|
30
|
+
expect(store.foo).toEqual('bar');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { reactive, toRaw, watch } from 'vue';
|
|
2
|
+
import { Storage } from '@noeldemartin/utils';
|
|
3
|
+
import type { UnwrapNestedRefs } from 'vue';
|
|
4
|
+
|
|
5
|
+
export function persistent<T extends object>(name: string, defaults: T): UnwrapNestedRefs<T> {
|
|
6
|
+
const store = reactive<T>(Storage.get<T>(name) ?? defaults);
|
|
7
|
+
|
|
8
|
+
watch(store, () => Storage.set(name, toRaw(store)));
|
|
9
|
+
|
|
10
|
+
return store;
|
|
11
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { after } from '@noeldemartin/utils';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ref } from 'vue';
|
|
4
|
+
|
|
5
|
+
import { computedDebounce } from './state';
|
|
6
|
+
|
|
7
|
+
describe('Vue state helpers', () => {
|
|
8
|
+
|
|
9
|
+
it('computes debounced state', async () => {
|
|
10
|
+
// Initial
|
|
11
|
+
const state = ref(0);
|
|
12
|
+
const value = computedDebounce({ delay: 90 }, () => state.value);
|
|
13
|
+
|
|
14
|
+
expect(value.value).toBe(null);
|
|
15
|
+
|
|
16
|
+
await after({ ms: 100 });
|
|
17
|
+
|
|
18
|
+
expect(value.value).toBe(0);
|
|
19
|
+
|
|
20
|
+
// Update
|
|
21
|
+
state.value = 42;
|
|
22
|
+
|
|
23
|
+
expect(value.value).toBe(0);
|
|
24
|
+
|
|
25
|
+
await after({ ms: 100 });
|
|
26
|
+
|
|
27
|
+
expect(value.value).toBe(42);
|
|
28
|
+
|
|
29
|
+
// Debounced Update
|
|
30
|
+
state.value = 23;
|
|
31
|
+
|
|
32
|
+
expect(value.value).toBe(42);
|
|
33
|
+
|
|
34
|
+
await after({ ms: 50 });
|
|
35
|
+
|
|
36
|
+
state.value = 32;
|
|
37
|
+
|
|
38
|
+
await after({ ms: 50 });
|
|
39
|
+
|
|
40
|
+
expect(value.value).toBe(42);
|
|
41
|
+
|
|
42
|
+
await after({ ms: 100 });
|
|
43
|
+
|
|
44
|
+
expect(value.value).toBe(32);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { debounce } from '@noeldemartin/utils';
|
|
2
|
+
import { ref, watchEffect } from 'vue';
|
|
3
|
+
import type { ComputedGetter, ComputedRef } from '@vue/runtime-core';
|
|
4
|
+
|
|
5
|
+
export interface ComputedDebounceOptions<T> {
|
|
6
|
+
initial?: T;
|
|
7
|
+
delay?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function computedDebounce<T>(options: ComputedDebounceOptions<T>, getter: ComputedGetter<T>): ComputedRef<T>;
|
|
11
|
+
export function computedDebounce<T>(getter: ComputedGetter<T>): ComputedRef<T | null>;
|
|
12
|
+
export function computedDebounce<T>(
|
|
13
|
+
optionsOrGetter: ComputedGetter<T> | ComputedDebounceOptions<T>,
|
|
14
|
+
inputGetter?: ComputedGetter<T>,
|
|
15
|
+
): ComputedRef<T> {
|
|
16
|
+
const inputOptions = inputGetter ? (optionsOrGetter as ComputedDebounceOptions<T>) : {};
|
|
17
|
+
const getter = inputGetter ?? (optionsOrGetter as ComputedGetter<T>);
|
|
18
|
+
const state = ref(inputOptions.initial ?? null);
|
|
19
|
+
const update = debounce((value) => (state.value = value), inputOptions.delay ?? 300);
|
|
20
|
+
|
|
21
|
+
watchEffect(() => update(getter()));
|
|
22
|
+
|
|
23
|
+
return state as unknown as ComputedRef<T>;
|
|
24
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* eslint-disable max-len */
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { renderMarkdown } from './markdown';
|
|
5
|
+
|
|
6
|
+
describe('Markdown utils', () => {
|
|
7
|
+
|
|
8
|
+
it('renders basic markdown', () => {
|
|
9
|
+
// Arrange
|
|
10
|
+
const expectedHTML = `
|
|
11
|
+
<h1>Title</h1>
|
|
12
|
+
<p>body with <a target="_blank" href="https://example.com">link</a></p>
|
|
13
|
+
<ul>
|
|
14
|
+
<li>One</li>
|
|
15
|
+
<li>Two</li>
|
|
16
|
+
<li>Three</li>
|
|
17
|
+
</ul>
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
// Act
|
|
21
|
+
const html = renderMarkdown(
|
|
22
|
+
['# Title', 'body with [link](https://example.com)', '- One', '- Two', '- Three'].join('\n'),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
// Assert
|
|
26
|
+
expect(normalizeHTML(html)).toMatch(new RegExp(normalizeHTML(expectedHTML)));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('renders button links', () => {
|
|
30
|
+
// Arrange
|
|
31
|
+
const expectedHTML = `
|
|
32
|
+
<p><button type="button" data-markdown-action="do-something">link</button></p>
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
const html = renderMarkdown('[link](#action:do-something)');
|
|
37
|
+
|
|
38
|
+
// Assert
|
|
39
|
+
expect(normalizeHTML(html)).toMatch(new RegExp(normalizeHTML(expectedHTML)));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
function normalizeHTML(html: string): string {
|
|
45
|
+
return html
|
|
46
|
+
.split('\n')
|
|
47
|
+
.map((line) => line.trim())
|
|
48
|
+
.join('\n')
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
package/src/utils/markdown.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { tap } from '@noeldemartin/utils';
|
|
2
1
|
import DOMPurify from 'dompurify';
|
|
2
|
+
import { stringMatchAll, tap } from '@noeldemartin/utils';
|
|
3
3
|
import { Renderer, marked } from 'marked';
|
|
4
4
|
|
|
5
5
|
function makeRenderer(): Renderer {
|
|
@@ -10,8 +10,23 @@ function makeRenderer(): Renderer {
|
|
|
10
10
|
});
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function renderActionLinks(html: string): string {
|
|
14
|
+
const matches = stringMatchAll<3>(html, /<a[^>]*href="#action:([^"]+)"[^>]*>([^<]+)<\/a>/g);
|
|
15
|
+
|
|
16
|
+
for (const [link, action, text] of matches) {
|
|
17
|
+
html = html.replace(link, `<button type="button" data-markdown-action="${action}">${text}</button>`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return html;
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
export function renderMarkdown(markdown: string): string {
|
|
14
|
-
|
|
24
|
+
let html = marked(markdown, { mangle: false, headerIds: false, renderer: makeRenderer() });
|
|
25
|
+
|
|
26
|
+
html = safeHtml(html);
|
|
27
|
+
html = renderActionLinks(html);
|
|
28
|
+
|
|
29
|
+
return html;
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
export function safeHtml(html: string): string {
|
package/src/utils/vue.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fail } from '@noeldemartin/utils';
|
|
2
2
|
import { computed, inject, reactive, ref, watch } from 'vue';
|
|
3
|
-
import type { Directive, InjectionKey, PropType, Ref, UnwrapNestedRefs } from 'vue';
|
|
3
|
+
import type { Directive, InjectionKey, MaybeRef, PropType, Ref, UnwrapNestedRefs } from 'vue';
|
|
4
4
|
|
|
5
5
|
type BaseProp<T> = {
|
|
6
6
|
type?: PropType<T>;
|
|
@@ -10,7 +10,10 @@ type BaseProp<T> = {
|
|
|
10
10
|
type RequiredProp<T> = BaseProp<T> & { required: true };
|
|
11
11
|
type OptionalProp<T> = BaseProp<T> & { default: T | (() => T) | null };
|
|
12
12
|
|
|
13
|
+
export type AcceptRefs<T> = { [K in keyof T]: T[K] | RefUnion<T[K]> };
|
|
13
14
|
export type ComponentProps = Record<string, unknown>;
|
|
15
|
+
export type RefUnion<T> = T extends infer R ? Ref<R> : never;
|
|
16
|
+
export type Unref<T> = { [K in keyof T]: T[K] extends MaybeRef<infer Value> ? Value : T[K] };
|
|
14
17
|
|
|
15
18
|
export function arrayProp<T>(defaultValue?: () => T[]): OptionalProp<T[]> {
|
|
16
19
|
return {
|