5htp-core 0.6.0 → 0.6.1-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/client/app/component.tsx +1 -0
- package/client/assets/css/colors.less +46 -25
- package/client/assets/css/components/button.less +14 -5
- package/client/assets/css/components/card.less +5 -10
- package/client/assets/css/components/mantine.less +6 -5
- package/client/assets/css/components/table.less +1 -1
- package/client/assets/css/text/icons.less +1 -1
- package/client/assets/css/text/text.less +4 -0
- package/client/assets/css/utils/borders.less +1 -1
- package/client/assets/css/utils/layouts.less +8 -5
- package/client/components/Button.tsx +20 -17
- package/client/components/Checkbox.tsx +6 -1
- package/client/components/ConnectedInput.tsx +34 -0
- package/client/components/DropDown.tsx +21 -4
- package/client/components/Input.tsx +2 -2
- package/client/components/Rte/Editor.tsx +23 -9
- package/client/components/Rte/ToolbarPlugin/ElementFormat.tsx +1 -1
- package/client/components/Rte/ToolbarPlugin/index.tsx +272 -183
- package/client/components/Rte/currentEditor.ts +31 -2
- package/client/components/Rte/index.tsx +3 -0
- package/client/components/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +4 -1
- package/client/components/Select.tsx +29 -16
- package/client/components/Table/index.tsx +27 -11
- package/client/components/containers/Popover/index.tsx +21 -4
- package/client/components/index.ts +4 -2
- package/client/services/router/index.tsx +7 -5
- package/common/errors/index.tsx +27 -3
- package/common/router/index.ts +4 -1
- package/common/utils/rte.ts +183 -0
- package/package.json +3 -2
- package/server/app/container/console/index.ts +62 -42
- package/server/app/container/index.ts +4 -0
- package/server/app/service/index.ts +4 -2
- package/server/services/auth/index.ts +28 -14
- package/server/services/auth/router/index.ts +1 -1
- package/server/services/auth/router/request.ts +4 -4
- package/server/services/email/index.ts +8 -51
- package/server/services/prisma/Facet.ts +118 -0
- package/server/services/prisma/index.ts +24 -0
- package/server/services/router/http/index.ts +0 -2
- package/server/services/router/index.ts +220 -86
- package/server/services/router/response/index.ts +0 -15
- package/server/utils/rte.ts +21 -132
- package/types/global/utils.d.ts +4 -22
- package/types/icons.d.ts +1 -1
- package/server/services/email/service.json +0 -6
- package/server/services/email/templates.ts +0 -49
- package/server/services/email/transporter.ts +0 -31
|
@@ -11,20 +11,21 @@
|
|
|
11
11
|
|
|
12
12
|
// Node
|
|
13
13
|
// Npm
|
|
14
|
+
import got from 'got';
|
|
15
|
+
import hInterval from 'human-interval';
|
|
14
16
|
import type express from 'express';
|
|
15
17
|
import type { Request, Response, NextFunction } from 'express';
|
|
16
18
|
import { v4 as uuid } from 'uuid';
|
|
17
|
-
import zod from 'zod';
|
|
19
|
+
import zod, { ZodError } from 'zod';
|
|
18
20
|
export { default as schema } from 'zod';
|
|
19
21
|
import type { GlobImportedWithMetas } from 'babel-plugin-glob-import';
|
|
20
22
|
|
|
21
23
|
// Core
|
|
22
24
|
import type { Application } from '@server/app';
|
|
23
25
|
import Service, { AnyService, TServiceArgs } from '@server/app/service';
|
|
24
|
-
import type { TRegisteredServicesIndex } from '@server/app/service/container';
|
|
25
26
|
import context from '@server/context';
|
|
26
27
|
import type DisksManager from '@server/services/disks';
|
|
27
|
-
import { CoreError, NotFound, toJson as errorToJson } from '@common/errors';
|
|
28
|
+
import { CoreError, InputError, NotFound, toJson as errorToJson } from '@common/errors';
|
|
28
29
|
import BaseRouter, {
|
|
29
30
|
TRoute, TErrorRoute, TRouteModule,
|
|
30
31
|
TRouteOptions, defaultOptions,
|
|
@@ -138,7 +139,13 @@ export default class ServerRouter
|
|
|
138
139
|
public ssrRoutes: TSsrUnresolvedRoute[] = [];
|
|
139
140
|
|
|
140
141
|
// Cache (ex: for static pages)
|
|
141
|
-
public cache: {
|
|
142
|
+
public cache: {
|
|
143
|
+
[pageId: string]: {
|
|
144
|
+
rendered: any,
|
|
145
|
+
expire: number | undefined,
|
|
146
|
+
options: TRouteOptions["static"]
|
|
147
|
+
}
|
|
148
|
+
} = {}
|
|
142
149
|
|
|
143
150
|
/*----------------------------------
|
|
144
151
|
- SERVICE
|
|
@@ -158,6 +165,11 @@ export default class ServerRouter
|
|
|
158
165
|
|
|
159
166
|
public async ready() {
|
|
160
167
|
|
|
168
|
+
// Every hours
|
|
169
|
+
setInterval(() => {
|
|
170
|
+
this.refreshStaticPages();
|
|
171
|
+
}, 1000 * 60 * 60);
|
|
172
|
+
|
|
161
173
|
// Detect router services
|
|
162
174
|
for (const serviceName in this.config.plugins) {
|
|
163
175
|
this.app.register( this.config.plugins[serviceName] )
|
|
@@ -173,6 +185,32 @@ export default class ServerRouter
|
|
|
173
185
|
// Start HTTP server
|
|
174
186
|
await this.http.start();
|
|
175
187
|
|
|
188
|
+
// override
|
|
189
|
+
const originalLog = console.log;
|
|
190
|
+
console.log = (...args: any[]) => {
|
|
191
|
+
|
|
192
|
+
// parse stack trace: skip this function and the console.log call
|
|
193
|
+
/*const stackLine = (new Error()).stack?.split('\n')[2] || '';
|
|
194
|
+
const match = stackLine.match(/at (\w+)\.(\w+) /);
|
|
195
|
+
const className = match ? match[1] : '<global>';
|
|
196
|
+
const methodName = match ? match[2] : '<anonymous>';*/
|
|
197
|
+
|
|
198
|
+
const contextData = context.getStore() || {
|
|
199
|
+
channelType: 'master',
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const requestPrefix = contextData.channelType === 'request'
|
|
203
|
+
? `[${contextData.user ? contextData.user : 'guest'}] ${contextData.method} ${contextData.path} |`
|
|
204
|
+
: 'master';
|
|
205
|
+
|
|
206
|
+
// prefix and forward
|
|
207
|
+
originalLog.call(
|
|
208
|
+
console,
|
|
209
|
+
`${requestPrefix}`, // ${className}.${methodName}
|
|
210
|
+
...args
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
176
214
|
}
|
|
177
215
|
|
|
178
216
|
public async shutdown() {
|
|
@@ -183,6 +221,65 @@ export default class ServerRouter
|
|
|
183
221
|
- ACTIONS
|
|
184
222
|
----------------------------------*/
|
|
185
223
|
|
|
224
|
+
public async renderStatic(
|
|
225
|
+
path: string,
|
|
226
|
+
options: TRouteOptions["static"],
|
|
227
|
+
rendered?: any
|
|
228
|
+
) {
|
|
229
|
+
|
|
230
|
+
// Wildcard: tell that the newly rendered pages should be cached
|
|
231
|
+
if (path === '*' || !path)
|
|
232
|
+
return;
|
|
233
|
+
|
|
234
|
+
if (!rendered) {
|
|
235
|
+
|
|
236
|
+
const fullUrl = this.url(path, {}, true);
|
|
237
|
+
console.log('[router] renderStatic', fullUrl);
|
|
238
|
+
|
|
239
|
+
const response = await got( fullUrl, {
|
|
240
|
+
method: 'GET',
|
|
241
|
+
headers: {
|
|
242
|
+
'Accept': 'text/html'
|
|
243
|
+
},
|
|
244
|
+
throwHttpErrors: false,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
if (response.statusCode !== 200) {
|
|
248
|
+
console.error('renderStatic', response.statusCode, response.body);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
rendered = response.body;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
this.cache[path] = {
|
|
256
|
+
rendered: rendered,
|
|
257
|
+
options: options,
|
|
258
|
+
expire: typeof options === 'object'
|
|
259
|
+
? Date.now() + (hInterval(options.refresh) || 3600)
|
|
260
|
+
: undefined
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private refreshStaticPages() {
|
|
266
|
+
|
|
267
|
+
console.log('[router] refreshStaticPages');
|
|
268
|
+
|
|
269
|
+
for (const pageId in this.cache) {
|
|
270
|
+
const page = this.cache[pageId];
|
|
271
|
+
if (page.path && page.expire && page.expire < Date.now()) {
|
|
272
|
+
|
|
273
|
+
this.renderStatic(page.path, page.options);
|
|
274
|
+
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
|
|
186
283
|
private registerRoutes(defModules: GlobImportedWithMetas<TRouteModule>) {
|
|
187
284
|
for (const routeModule of defModules) {
|
|
188
285
|
|
|
@@ -231,6 +328,14 @@ export default class ServerRouter
|
|
|
231
328
|
|
|
232
329
|
this.routes.push(route);
|
|
233
330
|
|
|
331
|
+
// Add to static pages
|
|
332
|
+
// Should be a GET oage that don't take any parameter
|
|
333
|
+
if (options.static) {
|
|
334
|
+
for (const url of options.static.urls) {
|
|
335
|
+
this.renderStatic(url, options.static);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
234
339
|
return this;
|
|
235
340
|
|
|
236
341
|
}
|
|
@@ -393,7 +498,13 @@ export default class ServerRouter
|
|
|
393
498
|
"Cache-Control",
|
|
394
499
|
"no-store, no-cache, must-revalidate, proxy-revalidate"
|
|
395
500
|
);
|
|
396
|
-
|
|
501
|
+
|
|
502
|
+
// Static pages
|
|
503
|
+
if (this.cache[req.path]) {
|
|
504
|
+
console.log('[router] Get static page from cache', req.path);
|
|
505
|
+
res.send( this.cache[req.path].rendered );
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
397
508
|
|
|
398
509
|
// Create request
|
|
399
510
|
let requestId = uuid();
|
|
@@ -410,39 +521,34 @@ export default class ServerRouter
|
|
|
410
521
|
this
|
|
411
522
|
);
|
|
412
523
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
let response: ServerResponse<this>;
|
|
417
|
-
try {
|
|
418
|
-
|
|
419
|
-
// Hook
|
|
420
|
-
await this.runHook('request', request);
|
|
524
|
+
let response: ServerResponse<this>;
|
|
525
|
+
try {
|
|
421
526
|
|
|
422
|
-
|
|
423
|
-
|
|
527
|
+
// Hook
|
|
528
|
+
await this.runHook('request', request);
|
|
424
529
|
|
|
425
|
-
|
|
530
|
+
// Bulk API Requests
|
|
531
|
+
if (request.path === '/api' && typeof request.data.fetchers === "object") {
|
|
426
532
|
|
|
427
|
-
|
|
428
|
-
response = await this.resolve(request);
|
|
429
|
-
}
|
|
430
|
-
} catch (e) {
|
|
431
|
-
response = await this.handleError(e, request);
|
|
432
|
-
}
|
|
533
|
+
return await this.resolveApiBatch(request.data.fetchers, request);
|
|
433
534
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
res.status(response.statusCode);
|
|
437
|
-
// Headers
|
|
438
|
-
res.header(response.headers);
|
|
439
|
-
// Data
|
|
440
|
-
res.send(response.data);
|
|
441
|
-
} else if (response.data !== 'true') {
|
|
442
|
-
throw new Error("Can't return data from the controller since response has already been sent via express.");
|
|
535
|
+
} else {
|
|
536
|
+
response = await this.resolve(request);
|
|
443
537
|
}
|
|
538
|
+
} catch (e) {
|
|
539
|
+
response = await this.handleError(e, request);
|
|
540
|
+
}
|
|
444
541
|
|
|
445
|
-
|
|
542
|
+
if (!res.headersSent) {
|
|
543
|
+
// Status
|
|
544
|
+
res.status(response.statusCode);
|
|
545
|
+
// Headers
|
|
546
|
+
res.header(response.headers);
|
|
547
|
+
// Data
|
|
548
|
+
res.send(response.data);
|
|
549
|
+
} else if (response.data !== 'true') {
|
|
550
|
+
throw new Error("Can't return data from the controller since response has already been sent via express.");
|
|
551
|
+
}
|
|
446
552
|
}
|
|
447
553
|
|
|
448
554
|
public createContextServices( request: ServerRequest<this> ) {
|
|
@@ -466,75 +572,86 @@ export default class ServerRouter
|
|
|
466
572
|
return contextServices;
|
|
467
573
|
}
|
|
468
574
|
|
|
469
|
-
public
|
|
575
|
+
public resolve = (request: ServerRequest<this>) => new Promise<ServerResponse<this>>((resolve, reject) => {
|
|
576
|
+
|
|
577
|
+
// Create request context so we can access request context across all the request-triggered libs
|
|
578
|
+
context.run({
|
|
579
|
+
// This is for debugging
|
|
580
|
+
channelType: 'request',
|
|
581
|
+
channelId: request.id,
|
|
582
|
+
method: request.method,
|
|
583
|
+
path: request.path,
|
|
584
|
+
}, async () => {
|
|
585
|
+
|
|
586
|
+
const timeStart = Date.now();
|
|
587
|
+
|
|
588
|
+
if (this.status === 'starting') {
|
|
589
|
+
console.log(LogPrefix, `Waiting for servert to be resdy before resolving request`);
|
|
590
|
+
await this.started;
|
|
591
|
+
}
|
|
470
592
|
|
|
471
|
-
|
|
472
|
-
console.info(logId);
|
|
473
|
-
const timeStart = Date.now();
|
|
593
|
+
try {
|
|
474
594
|
|
|
475
|
-
|
|
476
|
-
console.log(LogPrefix, `Waiting for servert to be resdy before resolving request`);
|
|
477
|
-
await this.started;
|
|
478
|
-
}
|
|
595
|
+
const response = new ServerResponse<this>(request);
|
|
479
596
|
|
|
480
|
-
|
|
597
|
+
await this.runHook('resolve', request);
|
|
481
598
|
|
|
482
|
-
|
|
599
|
+
// Controller route
|
|
600
|
+
let route = this.controllers[request.path];
|
|
601
|
+
if (route !== undefined) {
|
|
483
602
|
|
|
484
|
-
|
|
603
|
+
// Create response
|
|
604
|
+
await this.resolvedRoute(route, response, timeStart);
|
|
605
|
+
if (response.wasProvided)
|
|
606
|
+
return resolve(response);
|
|
607
|
+
}
|
|
485
608
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
609
|
+
const contextStore = context.getStore();
|
|
610
|
+
if (contextStore)
|
|
611
|
+
contextStore.user = request.user?.email;
|
|
489
612
|
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
if (response.wasProvided)
|
|
493
|
-
return response;
|
|
494
|
-
}
|
|
613
|
+
// Classic routes
|
|
614
|
+
for (route of this.routes) {
|
|
495
615
|
|
|
496
|
-
|
|
497
|
-
|
|
616
|
+
// Match Method
|
|
617
|
+
if (request.method !== route.method && route.method !== '*')
|
|
618
|
+
continue;
|
|
498
619
|
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
620
|
+
// Match Response format
|
|
621
|
+
if (!request.accepts(route.options.accept))
|
|
622
|
+
continue;
|
|
502
623
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
624
|
+
const isMatching = matchRoute(route, request);
|
|
625
|
+
if (!isMatching)
|
|
626
|
+
continue;
|
|
506
627
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
628
|
+
await this.resolvedRoute(route, response, timeStart);
|
|
629
|
+
if (response.wasProvided)
|
|
630
|
+
return resolve(response);
|
|
631
|
+
}
|
|
510
632
|
|
|
511
|
-
|
|
512
|
-
if (response.wasProvided)
|
|
513
|
-
return response;
|
|
514
|
-
}
|
|
633
|
+
reject( new NotFound() );
|
|
515
634
|
|
|
516
|
-
|
|
635
|
+
} catch (error) {
|
|
517
636
|
|
|
518
|
-
|
|
637
|
+
if (this.app.env.profile === 'dev') {
|
|
638
|
+
console.log('API batch error:', request.method, request.path, error);
|
|
639
|
+
const errOrigin = request.method + ' ' + request.path;
|
|
640
|
+
if (error.details === undefined)
|
|
641
|
+
error.details = { origin: errOrigin }
|
|
642
|
+
else
|
|
643
|
+
error.details.origin = errOrigin;
|
|
644
|
+
}
|
|
519
645
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
const errOrigin = request.method + ' ' + request.path;
|
|
523
|
-
if (error.details === undefined)
|
|
524
|
-
error.details = { origin: errOrigin }
|
|
525
|
-
else
|
|
526
|
-
error.details.origin = errOrigin;
|
|
646
|
+
this.printTakenTime(timeStart);
|
|
647
|
+
reject( error );
|
|
527
648
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
649
|
+
});
|
|
650
|
+
});
|
|
533
651
|
|
|
534
652
|
private async resolvedRoute(
|
|
535
653
|
route: TRoute,
|
|
536
654
|
response: ServerResponse<this>,
|
|
537
|
-
logId: string,
|
|
538
655
|
timeStart: number
|
|
539
656
|
) {
|
|
540
657
|
|
|
@@ -546,15 +663,25 @@ export default class ServerRouter
|
|
|
546
663
|
if (!response.wasProvided)
|
|
547
664
|
return;
|
|
548
665
|
|
|
666
|
+
// Set in cache
|
|
667
|
+
if (
|
|
668
|
+
response.request.path
|
|
669
|
+
&& route.options.static
|
|
670
|
+
&& route.options.static.urls.includes('*')
|
|
671
|
+
) {
|
|
672
|
+
console.log('[router] Set in cache', response.request.path);
|
|
673
|
+
this.renderStatic(response.request.path, route.options.static, response.data);
|
|
674
|
+
}
|
|
675
|
+
|
|
549
676
|
const timeEndResolving = Date.now();
|
|
550
|
-
this.printTakenTime(
|
|
677
|
+
this.printTakenTime(timeStart, timeEndResolving);
|
|
551
678
|
}
|
|
552
679
|
|
|
553
|
-
private printTakenTime = (
|
|
680
|
+
private printTakenTime = (timeStart: number, timeEndResolving?: number) => {
|
|
554
681
|
|
|
555
682
|
if (this.app.env.name === 'server') return;
|
|
556
683
|
|
|
557
|
-
console.log(
|
|
684
|
+
console.log( Math.round(Date.now() - timeStart) + 'ms' +
|
|
558
685
|
(timeEndResolving === undefined ? '' : ' | Routing: ' + Math.round(timeEndResolving - timeStart))
|
|
559
686
|
);
|
|
560
687
|
}
|
|
@@ -583,7 +710,12 @@ export default class ServerRouter
|
|
|
583
710
|
request.res.json(responseData);
|
|
584
711
|
}
|
|
585
712
|
|
|
586
|
-
private async handleError( e: CoreError, request: ServerRequest<ServerRouter> ) {
|
|
713
|
+
private async handleError( e: Error |CoreError | ZodError, request: ServerRequest<ServerRouter> ) {
|
|
714
|
+
|
|
715
|
+
if (e instanceof ZodError)
|
|
716
|
+
e = new InputError(
|
|
717
|
+
e.errors.map(e => e.path.join('.') + ': ' + e.message).join(', ')
|
|
718
|
+
);
|
|
587
719
|
|
|
588
720
|
const code = 'http' in e ? e.http : 500;
|
|
589
721
|
|
|
@@ -600,7 +732,9 @@ export default class ServerRouter
|
|
|
600
732
|
|
|
601
733
|
// Don't exose technical errors to users
|
|
602
734
|
if (this.app.env.profile === 'prod')
|
|
603
|
-
e
|
|
735
|
+
e = new Error(
|
|
736
|
+
"We encountered an internal error, and our team has just been notified. Sorry for the inconvenience."
|
|
737
|
+
);
|
|
604
738
|
|
|
605
739
|
} else {
|
|
606
740
|
|
|
@@ -111,17 +111,6 @@ export default class ServerResponse<
|
|
|
111
111
|
// Create response context for controllers
|
|
112
112
|
const context = await this.createContext(route);
|
|
113
113
|
|
|
114
|
-
// Static rendering
|
|
115
|
-
const chunkId = route.options["id"];
|
|
116
|
-
if (route.options.static &&
|
|
117
|
-
chunkId !== undefined
|
|
118
|
-
&&
|
|
119
|
-
this.router.cache[ chunkId ] !== undefined
|
|
120
|
-
) {
|
|
121
|
-
await this.html( this.router.cache[ chunkId ] );
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
114
|
// Run controller
|
|
126
115
|
const content = await this.route.controller( context );
|
|
127
116
|
if (content === undefined)
|
|
@@ -139,10 +128,6 @@ export default class ServerResponse<
|
|
|
139
128
|
// Return JSON
|
|
140
129
|
else
|
|
141
130
|
await this.json(content);
|
|
142
|
-
|
|
143
|
-
// Cache
|
|
144
|
-
if (route.options.static)
|
|
145
|
-
this.router.cache[ chunkId ] = this.data;
|
|
146
131
|
}
|
|
147
132
|
|
|
148
133
|
private updateCanonicalUrl( route: TAnyRoute ) {
|
package/server/utils/rte.ts
CHANGED
|
@@ -9,131 +9,39 @@ import path from 'path';
|
|
|
9
9
|
import md5 from 'md5';
|
|
10
10
|
import { fromBuffer } from 'file-type';
|
|
11
11
|
import { JSDOM } from 'jsdom';
|
|
12
|
-
// Lexical
|
|
13
|
-
import { $getRoot } from 'lexical';
|
|
14
|
-
import { createHeadlessEditor } from '@lexical/headless';
|
|
15
|
-
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
16
12
|
|
|
17
13
|
// Core
|
|
18
|
-
import { Anomaly } from '@common/errors';
|
|
19
14
|
import editorNodes from '@common/data/rte/nodes';
|
|
20
15
|
import ExampleTheme from '@client/components/Rte/themes/PlaygroundEditorTheme';
|
|
21
|
-
import type Driver from '@server/services/disks/driver';
|
|
22
16
|
import Slug from '@server/utils/slug';
|
|
23
17
|
|
|
18
|
+
// Lexical
|
|
19
|
+
import { $getRoot, SerializedEditorState, SerializedLexicalNode } from 'lexical';
|
|
20
|
+
import { createHeadlessEditor } from '@lexical/headless';
|
|
21
|
+
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
22
|
+
|
|
23
|
+
// Specifc
|
|
24
|
+
import {
|
|
25
|
+
default as BaseRteUtils,
|
|
26
|
+
LexicalNode,
|
|
27
|
+
LexicalState,
|
|
28
|
+
TRenderOptions,
|
|
29
|
+
TContentAssets,
|
|
30
|
+
TSkeleton
|
|
31
|
+
} from '@common/utils/rte';
|
|
32
|
+
|
|
24
33
|
/*----------------------------------
|
|
25
34
|
- TYPES
|
|
26
35
|
----------------------------------*/
|
|
27
36
|
|
|
28
|
-
type LexicalState = {
|
|
29
|
-
root: LexicalNode
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type LexicalNode = {
|
|
33
|
-
version: number,
|
|
34
|
-
type: string,
|
|
35
|
-
children?: LexicalNode[],
|
|
36
|
-
// Attachement
|
|
37
|
-
src?: string;
|
|
38
|
-
// Headhing
|
|
39
|
-
text?: string;
|
|
40
|
-
anchor?: string;
|
|
41
|
-
tag?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type TRenderOptions = {
|
|
45
|
-
|
|
46
|
-
transform?: RteUtils["transformNode"],
|
|
47
|
-
|
|
48
|
-
render?: (
|
|
49
|
-
node: LexicalNode,
|
|
50
|
-
parent: LexicalNode | null,
|
|
51
|
-
options: TRenderOptions
|
|
52
|
-
) => Promise<LexicalNode>,
|
|
53
|
-
|
|
54
|
-
attachements?: {
|
|
55
|
-
disk: Driver,
|
|
56
|
-
directory: string,
|
|
57
|
-
prevVersion?: string | LexicalState | null,
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
type TSkeleton = {
|
|
62
|
-
id: string,
|
|
63
|
-
title: string,
|
|
64
|
-
level: number,
|
|
65
|
-
childrens: TSkeleton
|
|
66
|
-
}[];
|
|
67
|
-
|
|
68
|
-
type TContentAssets = {
|
|
69
|
-
attachements: string[],
|
|
70
|
-
skeleton: TSkeleton
|
|
71
|
-
}
|
|
72
37
|
|
|
73
38
|
/*----------------------------------
|
|
74
39
|
- FUNCTIONS
|
|
75
40
|
----------------------------------*/
|
|
76
41
|
|
|
77
|
-
export class RteUtils {
|
|
78
|
-
|
|
79
|
-
public async render(
|
|
80
|
-
content: string | LexicalState,
|
|
81
|
-
options: TRenderOptions = {}
|
|
82
|
-
): Promise<TContentAssets & {
|
|
83
|
-
html: string,
|
|
84
|
-
json: string | LexicalState,
|
|
85
|
-
}> {
|
|
42
|
+
export class RteUtils extends BaseRteUtils {
|
|
86
43
|
|
|
87
|
-
|
|
88
|
-
const assets: TContentAssets = {
|
|
89
|
-
attachements: [],
|
|
90
|
-
skeleton: []
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Parse content if string
|
|
94
|
-
let json: LexicalState;
|
|
95
|
-
if (typeof content === 'string' && content.trim().startsWith('{')) {
|
|
96
|
-
try {
|
|
97
|
-
json = JSON.parse(content) as LexicalState;
|
|
98
|
-
} catch (error) {
|
|
99
|
-
throw new Anomaly("Invalid JSON format for the given JSON RTE content.");
|
|
100
|
-
}
|
|
101
|
-
} else if (content && typeof content === 'object' && content.root)
|
|
102
|
-
json = content;
|
|
103
|
-
else
|
|
104
|
-
return { html: '', json: content, ...assets };
|
|
105
|
-
|
|
106
|
-
// Parse prev version if string
|
|
107
|
-
if (typeof options?.attachements?.prevVersion === 'string') {
|
|
108
|
-
try {
|
|
109
|
-
options.attachements.prevVersion = JSON.parse(options.attachements.prevVersion) as LexicalState;
|
|
110
|
-
} catch (error) {
|
|
111
|
-
throw new Anomaly("Invalid JSON format for the given JSON RTE prev version.");
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const root = await this.processContent(json.root, null, async (node, parent) => {
|
|
116
|
-
return await this.transformNode(node, parent, assets, options);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
json = { ...json, root };
|
|
120
|
-
|
|
121
|
-
// Delete unused attachements
|
|
122
|
-
const attachementOptions = options?.attachements;
|
|
123
|
-
if (attachementOptions && attachementOptions.prevVersion !== undefined) {
|
|
124
|
-
|
|
125
|
-
await this.processContent(root, null, async (node) => {
|
|
126
|
-
return await this.deleteUnusedFile(node, assets, attachementOptions);
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Convert json to HTML
|
|
131
|
-
const html = await this.jsonToHtml( json, options );
|
|
132
|
-
|
|
133
|
-
return { html, json: content, ...assets };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
private async processContent(
|
|
44
|
+
protected async processContent(
|
|
137
45
|
node: LexicalNode,
|
|
138
46
|
parent: LexicalNode | null,
|
|
139
47
|
callback: (node: LexicalNode, parent: LexicalNode | null) => Promise<LexicalNode>
|
|
@@ -153,7 +61,7 @@ export class RteUtils {
|
|
|
153
61
|
return node;
|
|
154
62
|
}
|
|
155
63
|
|
|
156
|
-
|
|
64
|
+
protected async transformNode(
|
|
157
65
|
node: LexicalNode,
|
|
158
66
|
parent: LexicalNode | null,
|
|
159
67
|
assets: TContentAssets,
|
|
@@ -254,7 +162,7 @@ export class RteUtils {
|
|
|
254
162
|
});
|
|
255
163
|
}
|
|
256
164
|
|
|
257
|
-
|
|
165
|
+
protected async deleteUnusedFile(
|
|
258
166
|
node: LexicalNode,
|
|
259
167
|
assets: TContentAssets,
|
|
260
168
|
options: NonNullable<TRenderOptions["attachements"]>
|
|
@@ -282,7 +190,7 @@ export class RteUtils {
|
|
|
282
190
|
return node;
|
|
283
191
|
}
|
|
284
192
|
|
|
285
|
-
public async jsonToHtml( json: LexicalState, options: TRenderOptions = {} ) {
|
|
193
|
+
public async jsonToHtml( json: LexicalState, options: TRenderOptions = {} ): Promise<string | null> {
|
|
286
194
|
|
|
287
195
|
// Transform before rendering
|
|
288
196
|
const renderTransform = options.render;
|
|
@@ -310,7 +218,7 @@ export class RteUtils {
|
|
|
310
218
|
});
|
|
311
219
|
|
|
312
220
|
// Set the editor state from JSON
|
|
313
|
-
const state = editor.parseEditorState(json);
|
|
221
|
+
const state = editor.parseEditorState(json as SerializedEditorState<SerializedLexicalNode>);
|
|
314
222
|
if (state.isEmpty())
|
|
315
223
|
return '';
|
|
316
224
|
|
|
@@ -334,25 +242,6 @@ export class RteUtils {
|
|
|
334
242
|
return html;
|
|
335
243
|
}
|
|
336
244
|
|
|
337
|
-
private jsonToText( node: LexicalNode ) {
|
|
338
|
-
|
|
339
|
-
let text = '';
|
|
340
|
-
|
|
341
|
-
// Check if the node has text content
|
|
342
|
-
if (node.type === 'text' && node.text) {
|
|
343
|
-
text += node.text;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// Recursively process children nodes
|
|
347
|
-
if (node.children && Array.isArray(node.children)) {
|
|
348
|
-
node.children.forEach(childNode => {
|
|
349
|
-
text += this.jsonToText(childNode);
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return text;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
245
|
public async htmlToJson(htmlString: string): Promise<LexicalState> {
|
|
357
246
|
|
|
358
247
|
const editor = createHeadlessEditor({
|