5htp-core 0.5.1-3 → 0.5.1-5
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/package.json +1 -1
- package/src/client/components/inputv3/Rte/nodes/ReferenceLinkNode.tsx +56 -0
- package/src/client/components/inputv3/file/index.less +10 -8
- package/src/client/components/inputv3/file/index.tsx +48 -24
- package/src/common/data/rte/nodes.ts +4 -0
- package/src/server/services/router/request/index.ts +18 -1
- package/src/server/utils/rte.ts +42 -11
- package/src/server/utils/slug.ts +32 -20
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5htp-core",
|
|
3
3
|
"description": "Convenient TypeScript framework designed for Performance and Productivity.",
|
|
4
|
-
"version": "0.5.1-
|
|
4
|
+
"version": "0.5.1-5",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
|
|
2
|
+
import { HeadingNode, HeadingTagType } from '@lexical/rich-text';
|
|
3
|
+
import { AutoLinkNode, LinkNode } from '@lexical/link';
|
|
4
|
+
|
|
5
|
+
export default class ReferenceLinkNode extends LinkNode {
|
|
6
|
+
|
|
7
|
+
public referenceTo?: string;
|
|
8
|
+
|
|
9
|
+
// Adding a static method to register the custom node
|
|
10
|
+
static getType() {
|
|
11
|
+
return 'reference-link';
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static clone(node) {
|
|
15
|
+
return new ReferenceLinkNode(node.__url, {
|
|
16
|
+
rel: node.__rel,
|
|
17
|
+
target: node.__target,
|
|
18
|
+
title: node.__title
|
|
19
|
+
}, node.__key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Add a `referenceTo` attribute to the serialized JSON
|
|
23
|
+
static importJSON(serializedNode) {
|
|
24
|
+
const node = new ReferenceLinkNode(serializedNode.url);
|
|
25
|
+
node.referenceTo = serializedNode.referenceTo;
|
|
26
|
+
return node;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Ensure the `referenceTo` attribute is serialized in JSON
|
|
30
|
+
exportJSON() {
|
|
31
|
+
return {
|
|
32
|
+
...super.exportJSON(),
|
|
33
|
+
type: ReferenceLinkNode.getType(),
|
|
34
|
+
referenceTo: this.referenceTo
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Override createDOM to set the `referenceTo` attribute
|
|
39
|
+
createDOM(config) {
|
|
40
|
+
const dom = super.createDOM(config);
|
|
41
|
+
if (this.referenceTo) {
|
|
42
|
+
dom.setAttribute('class', this.referenceTo);
|
|
43
|
+
}
|
|
44
|
+
return dom;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Update the DOM to reflect changes in the `referenceTo` attribute
|
|
48
|
+
updateDOM(prevNode, dom, config) {
|
|
49
|
+
const updated = super.updateDOM(prevNode, dom, config);
|
|
50
|
+
if (this.referenceTo !== prevNode.referenceTo) {
|
|
51
|
+
dom.setAttribute('class', this.referenceTo);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
return updated;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -5,14 +5,16 @@
|
|
|
5
5
|
position: relative;
|
|
6
6
|
overflow: hidden;
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
8
|
+
&.box {
|
|
9
|
+
border-radius: @radius;
|
|
10
|
+
border: dashed 3px var(--cLine);
|
|
11
|
+
background: @cBgPage + #090909;
|
|
12
|
+
|
|
13
|
+
padding: @spacing;
|
|
14
|
+
width: 100%;
|
|
15
|
+
min-height: 200px;
|
|
16
|
+
height: 14.15rem;
|
|
17
|
+
}
|
|
16
18
|
|
|
17
19
|
.preview,
|
|
18
20
|
.indication,
|
|
@@ -6,7 +6,7 @@ import React from 'react';
|
|
|
6
6
|
import { ComponentChild } from 'preact';
|
|
7
7
|
|
|
8
8
|
// Composants généraux
|
|
9
|
-
import
|
|
9
|
+
import Button, { Props as BtnProps } from '@client/components/button';
|
|
10
10
|
|
|
11
11
|
// Core libs
|
|
12
12
|
import { InputWrapper } from '../base';
|
|
@@ -67,6 +67,7 @@ export type Props = {
|
|
|
67
67
|
emptyText?: ComponentChild,
|
|
68
68
|
className?: string,
|
|
69
69
|
previewUrl?: string,
|
|
70
|
+
button?: boolean | BtnProps,
|
|
70
71
|
|
|
71
72
|
// Actions
|
|
72
73
|
onChange: (file: FileToUpload | undefined) => void
|
|
@@ -85,11 +86,12 @@ export default (props: Props) => {
|
|
|
85
86
|
let {
|
|
86
87
|
// Input
|
|
87
88
|
value: file,
|
|
88
|
-
className,
|
|
89
|
+
className: customClassName,
|
|
89
90
|
|
|
90
91
|
// Display
|
|
91
92
|
emptyText = 'Click here to select a File',
|
|
92
93
|
previewUrl: previewUrlInit,
|
|
94
|
+
button,
|
|
93
95
|
|
|
94
96
|
// Actions
|
|
95
97
|
onChange
|
|
@@ -97,7 +99,13 @@ export default (props: Props) => {
|
|
|
97
99
|
|
|
98
100
|
const [previewUrl, setPreviewUrl] = React.useState<string | undefined>(previewUrlInit);
|
|
99
101
|
|
|
100
|
-
className = 'input upload
|
|
102
|
+
let className = 'input upload';
|
|
103
|
+
|
|
104
|
+
if (!button)
|
|
105
|
+
className += ' box';
|
|
106
|
+
|
|
107
|
+
if (customClassName !== undefined)
|
|
108
|
+
className += ' ' + customClassName;
|
|
101
109
|
|
|
102
110
|
/*----------------------------------
|
|
103
111
|
- ACTIONS
|
|
@@ -131,33 +139,49 @@ export default (props: Props) => {
|
|
|
131
139
|
|
|
132
140
|
<div class={className}>
|
|
133
141
|
|
|
134
|
-
{
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
{
|
|
138
|
-
|
|
139
|
-
|
|
142
|
+
{button ? <>
|
|
143
|
+
|
|
144
|
+
<Button type="secondary"
|
|
145
|
+
{...button === true ? {} : button}
|
|
146
|
+
>
|
|
147
|
+
{emptyText}
|
|
148
|
+
</Button>
|
|
149
|
+
|
|
150
|
+
</> : <>
|
|
151
|
+
|
|
152
|
+
{file && <>
|
|
153
|
+
<div class="preview">
|
|
154
|
+
|
|
155
|
+
{previewUrl ? (
|
|
156
|
+
<img src={previewUrl} />
|
|
157
|
+
) : typeof file === 'string' ? <>
|
|
158
|
+
<strong>A file has been selected</strong>
|
|
159
|
+
</> : file ? <>
|
|
160
|
+
<strong>{file.name}</strong>
|
|
161
|
+
</> : null}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div class="row actions sp-05">
|
|
165
|
+
|
|
166
|
+
{typeof file === 'string' && <>
|
|
167
|
+
<Button type="secondary" icon="eye" shape="pill" size="s" link={file} />
|
|
168
|
+
</>}
|
|
169
|
+
|
|
170
|
+
<Button class='bg error' icon="trash" shape="pill" size="s"
|
|
171
|
+
async onClick={() => onChange(undefined)} />
|
|
172
|
+
</div>
|
|
173
|
+
</>}
|
|
174
|
+
|
|
175
|
+
<div class="indication col al-center">
|
|
176
|
+
{typeof file === 'string' ? <>
|
|
140
177
|
<strong>A file has been selected</strong>
|
|
141
178
|
</> : file ? <>
|
|
142
179
|
<strong>{file.name}</strong>
|
|
143
|
-
</> :
|
|
144
|
-
</div>
|
|
145
|
-
|
|
146
|
-
<div class="row actions sp-05">
|
|
147
|
-
|
|
148
|
-
{typeof file === 'string' && <>
|
|
149
|
-
<Bouton type="secondary" icon="eye" shape="pill" size="s" link={file} />
|
|
150
|
-
</>}
|
|
151
|
-
|
|
152
|
-
<Bouton class='bg error' icon="trash" shape="pill" size="s"
|
|
153
|
-
async onClick={() => onChange(undefined)} />
|
|
180
|
+
</> : emptyText}
|
|
154
181
|
</div>
|
|
182
|
+
|
|
155
183
|
</>}
|
|
156
184
|
|
|
157
|
-
<div class="indication col al-center">
|
|
158
|
-
{emptyText}
|
|
159
|
-
</div>
|
|
160
|
-
|
|
161
185
|
<input type="file" onChange={selectFile} />
|
|
162
186
|
</div>
|
|
163
187
|
</InputWrapper>
|
|
@@ -36,6 +36,7 @@ import { TweetNode } from '@client/components/inputv3/Rte/nodes/TweetNode';
|
|
|
36
36
|
import { YouTubeNode } from '@client/components/inputv3/Rte/nodes/YouTubeNode';
|
|
37
37
|
|
|
38
38
|
import HeadingWithAnchorNode from '@client/components/inputv3/Rte/nodes/HeadingNode';
|
|
39
|
+
import ReferenceLinkNode from '@client/components/inputv3/Rte/nodes/ReferenceLinkNode';
|
|
39
40
|
|
|
40
41
|
const PlaygroundNodes: Array<Klass<LexicalNode>> = [
|
|
41
42
|
/*HeadingNode, */HeadingWithAnchorNode,
|
|
@@ -74,6 +75,9 @@ const PlaygroundNodes: Array<Klass<LexicalNode>> = [
|
|
|
74
75
|
PageBreakNode,
|
|
75
76
|
LayoutContainerNode,
|
|
76
77
|
LayoutItemNode,
|
|
78
|
+
|
|
79
|
+
// Custom
|
|
80
|
+
ReferenceLinkNode
|
|
77
81
|
];
|
|
78
82
|
|
|
79
83
|
export default PlaygroundNodes;
|
|
@@ -24,7 +24,21 @@ import ServerResponse from '../response';
|
|
|
24
24
|
- TYPES
|
|
25
25
|
----------------------------------*/
|
|
26
26
|
|
|
27
|
-
const localeFilter = (input: any) =>
|
|
27
|
+
const localeFilter = (input: any) => {
|
|
28
|
+
|
|
29
|
+
// Data type
|
|
30
|
+
if (typeof input !== 'string')
|
|
31
|
+
return;
|
|
32
|
+
|
|
33
|
+
// Extract ISO code
|
|
34
|
+
let lang = input.trim().split(/[-_]/)[0].toLowerCase();
|
|
35
|
+
|
|
36
|
+
// Check size
|
|
37
|
+
if (!ISO6391.validate(lang))
|
|
38
|
+
return;
|
|
39
|
+
|
|
40
|
+
return lang.toUpperCase();
|
|
41
|
+
}
|
|
28
42
|
|
|
29
43
|
export type UploadedFile = With<FileToUpload, 'md5'|'ext'>
|
|
30
44
|
|
|
@@ -132,6 +146,9 @@ export default class ServerRequest<
|
|
|
132
146
|
'EN'
|
|
133
147
|
)
|
|
134
148
|
|
|
149
|
+
console.log("locale", this.req.acceptsLanguages(), locale);
|
|
150
|
+
|
|
151
|
+
|
|
135
152
|
return locale ? locale.toUpperCase() : 'EN'
|
|
136
153
|
}
|
|
137
154
|
|
package/src/server/utils/rte.ts
CHANGED
|
@@ -29,7 +29,7 @@ type LexicalState = {
|
|
|
29
29
|
root: LexicalNode
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
type LexicalNode = {
|
|
32
|
+
export type LexicalNode = {
|
|
33
33
|
version: number,
|
|
34
34
|
type: string,
|
|
35
35
|
children?: LexicalNode[],
|
|
@@ -42,6 +42,15 @@ type LexicalNode = {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
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
|
+
|
|
45
54
|
attachements?: {
|
|
46
55
|
disk: Driver,
|
|
47
56
|
directory: string,
|
|
@@ -103,8 +112,8 @@ export class RteUtils {
|
|
|
103
112
|
}
|
|
104
113
|
}
|
|
105
114
|
|
|
106
|
-
const root = await this.processContent(json.root, async (node) => {
|
|
107
|
-
return await this.transformNode(node, assets, options);
|
|
115
|
+
const root = await this.processContent(json.root, null, async (node, parent) => {
|
|
116
|
+
return await this.transformNode(node, parent, assets, options);
|
|
108
117
|
});
|
|
109
118
|
|
|
110
119
|
json = { ...json, root };
|
|
@@ -113,29 +122,30 @@ export class RteUtils {
|
|
|
113
122
|
const attachementOptions = options?.attachements;
|
|
114
123
|
if (attachementOptions && attachementOptions.prevVersion !== undefined) {
|
|
115
124
|
|
|
116
|
-
await this.processContent(root, async (node) => {
|
|
125
|
+
await this.processContent(root, null, async (node) => {
|
|
117
126
|
return await this.deleteUnusedFile(node, assets, attachementOptions);
|
|
118
127
|
});
|
|
119
128
|
}
|
|
120
129
|
|
|
121
130
|
// Convert json to HTML
|
|
122
|
-
const html = await this.jsonToHtml( json );
|
|
131
|
+
const html = await this.jsonToHtml( json, options );
|
|
123
132
|
|
|
124
133
|
return { html, json: content, ...assets };
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
private async processContent(
|
|
128
137
|
node: LexicalNode,
|
|
129
|
-
|
|
138
|
+
parent: LexicalNode | null,
|
|
139
|
+
callback: (node: LexicalNode, parent: LexicalNode | null) => Promise<LexicalNode>
|
|
130
140
|
) {
|
|
131
141
|
|
|
132
|
-
node = await callback(node);
|
|
142
|
+
node = await callback(node, parent);
|
|
133
143
|
|
|
134
144
|
// Recursion
|
|
135
145
|
if (node.children) {
|
|
136
146
|
for (let i = 0; i < node.children.length; i++) {
|
|
137
147
|
|
|
138
|
-
node.children[ i ] = await this.processContent( node.children[ i ], callback );
|
|
148
|
+
node.children[ i ] = await this.processContent( node.children[ i ], node, callback );
|
|
139
149
|
|
|
140
150
|
}
|
|
141
151
|
}
|
|
@@ -143,23 +153,34 @@ export class RteUtils {
|
|
|
143
153
|
return node;
|
|
144
154
|
}
|
|
145
155
|
|
|
146
|
-
private async transformNode(
|
|
156
|
+
private async transformNode(
|
|
157
|
+
node: LexicalNode,
|
|
158
|
+
parent: LexicalNode | null,
|
|
159
|
+
assets: TContentAssets,
|
|
160
|
+
options: TRenderOptions
|
|
161
|
+
): Promise<LexicalNode> {
|
|
147
162
|
|
|
148
|
-
//
|
|
163
|
+
// Images and files
|
|
149
164
|
if (node.type === 'image' || node.type === 'file') {
|
|
150
165
|
|
|
166
|
+
// Upload images and files and replace blobs by URLs
|
|
151
167
|
await this.processAttachement(
|
|
152
168
|
node as With<LexicalNode, 'src'>,
|
|
153
169
|
assets,
|
|
154
170
|
options,
|
|
155
171
|
);
|
|
156
172
|
|
|
173
|
+
// Headings
|
|
157
174
|
} else if (node.type === 'anchored-heading') {
|
|
158
175
|
|
|
176
|
+
// Create skeleton
|
|
159
177
|
await this.processHeading(node, assets);
|
|
160
178
|
|
|
161
179
|
}
|
|
162
180
|
|
|
181
|
+
if (options.transform)
|
|
182
|
+
node = await options.transform(node, parent, assets, options);
|
|
183
|
+
|
|
163
184
|
return node;
|
|
164
185
|
}
|
|
165
186
|
|
|
@@ -261,7 +282,17 @@ export class RteUtils {
|
|
|
261
282
|
return node;
|
|
262
283
|
}
|
|
263
284
|
|
|
264
|
-
public async jsonToHtml( json: LexicalState ) {
|
|
285
|
+
public async jsonToHtml( json: LexicalState, options: TRenderOptions = {} ) {
|
|
286
|
+
|
|
287
|
+
// Transform before rendering
|
|
288
|
+
const renderTransform = options.render;
|
|
289
|
+
if (renderTransform)
|
|
290
|
+
json = {
|
|
291
|
+
...json,
|
|
292
|
+
root: await this.processContent(json.root, null, async (node, parent) => {
|
|
293
|
+
return await renderTransform(node, parent, options);
|
|
294
|
+
})
|
|
295
|
+
}
|
|
265
296
|
|
|
266
297
|
// Server side: simulate DOM environment
|
|
267
298
|
const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
|
package/src/server/utils/slug.ts
CHANGED
|
@@ -37,31 +37,43 @@ export class Slug {
|
|
|
37
37
|
|
|
38
38
|
// Check if already existing
|
|
39
39
|
if (SQL !== undefined) {
|
|
40
|
-
|
|
41
|
-
const escapedSlug = escapeStringRegexp(slug);
|
|
42
|
-
|
|
43
|
-
const duplicates = await SQL.selectVal<number>(`
|
|
44
|
-
SELECT
|
|
45
|
-
IF( ${column} LIKE ${SQL.esc(slug)},
|
|
46
|
-
1,
|
|
47
|
-
CAST(SUBSTRING_INDEX(slug, '-', -1) AS UNSIGNED)
|
|
48
|
-
) AS duplicates
|
|
49
|
-
FROM ${table}
|
|
50
|
-
WHERE
|
|
51
|
-
${column} LIKE ${SQL.esc(slug)}
|
|
52
|
-
OR
|
|
53
|
-
${column} REGEXP '^${escapedSlug}-[0-9]+$'
|
|
54
|
-
ORDER BY duplicates DESC
|
|
55
|
-
LIMIT 1
|
|
56
|
-
`);
|
|
57
|
-
|
|
58
|
-
if (duplicates && duplicates > 0)
|
|
59
|
-
slug += `-${duplicates + 1}`;
|
|
40
|
+
slug = await this.Correct(slug, SQL, table, column);
|
|
60
41
|
}
|
|
61
42
|
|
|
62
43
|
return slug;
|
|
63
44
|
}
|
|
64
45
|
|
|
46
|
+
public async Correct(
|
|
47
|
+
slug: string,
|
|
48
|
+
SQL: SQL,
|
|
49
|
+
table: string,
|
|
50
|
+
column: string
|
|
51
|
+
) {
|
|
52
|
+
|
|
53
|
+
const escapedSlug = escapeStringRegexp(slug);
|
|
54
|
+
|
|
55
|
+
const duplicates = await SQL.selectVal<number>(`
|
|
56
|
+
SELECT
|
|
57
|
+
IF( ${column} LIKE ${SQL.esc(slug)},
|
|
58
|
+
1,
|
|
59
|
+
CAST(SUBSTRING_INDEX(slug, '-', -1) AS UNSIGNED)
|
|
60
|
+
) AS duplicates
|
|
61
|
+
FROM ${table}
|
|
62
|
+
WHERE
|
|
63
|
+
${column} LIKE ${SQL.esc(slug)}
|
|
64
|
+
OR
|
|
65
|
+
${column} REGEXP '^${escapedSlug}-[0-9]+$'
|
|
66
|
+
ORDER BY duplicates DESC
|
|
67
|
+
LIMIT 1
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
if (duplicates && duplicates > 0)
|
|
71
|
+
slug += `-${duplicates + 1}`;
|
|
72
|
+
|
|
73
|
+
return slug;
|
|
74
|
+
|
|
75
|
+
}
|
|
76
|
+
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
export default new Slug;
|