@truedat/core 8.3.4 → 8.3.6
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 +4 -3
- package/src/components/Markdown.js +79 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@truedat/core",
|
|
3
|
-
"version": "8.3.
|
|
3
|
+
"version": "8.3.6",
|
|
4
4
|
"description": "Truedat Web Core",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@testing-library/jest-dom": "^6.6.3",
|
|
52
52
|
"@testing-library/react": "^16.3.0",
|
|
53
53
|
"@testing-library/user-event": "^14.6.1",
|
|
54
|
-
"@truedat/test": "8.3.
|
|
54
|
+
"@truedat/test": "8.3.6",
|
|
55
55
|
"identity-obj-proxy": "^3.0.0",
|
|
56
56
|
"jest": "^29.7.0",
|
|
57
57
|
"redux-saga-test-plan": "^4.0.6"
|
|
@@ -66,6 +66,7 @@
|
|
|
66
66
|
"@tiptap/starter-kit": "^3.20.0",
|
|
67
67
|
"@xyflow/react": "^12.6.4",
|
|
68
68
|
"axios": "^1.13.5",
|
|
69
|
+
"dompurify": "^3.3.3",
|
|
69
70
|
"elkjs": "^0.10.0",
|
|
70
71
|
"graphql": "^16.11.0",
|
|
71
72
|
"immutable": "^4.3.7",
|
|
@@ -96,5 +97,5 @@
|
|
|
96
97
|
"swr": "^2.3.3",
|
|
97
98
|
"turndown": "^7.2.2"
|
|
98
99
|
},
|
|
99
|
-
"gitHead": "
|
|
100
|
+
"gitHead": "767585383b6373e75fa50d7440e9ecf67b0573c2"
|
|
100
101
|
}
|
|
@@ -8,6 +8,7 @@ import Heading from "@tiptap/extension-heading";
|
|
|
8
8
|
import { Menu, Icon } from "semantic-ui-react";
|
|
9
9
|
import { FormattedMessage } from "react-intl";
|
|
10
10
|
import { marked } from "marked";
|
|
11
|
+
import DOMPurify from "dompurify";
|
|
11
12
|
import TurndownService from "turndown";
|
|
12
13
|
import TextPromptModal from "./TextPromptModal";
|
|
13
14
|
|
|
@@ -27,7 +28,7 @@ const extensions = [
|
|
|
27
28
|
rel: "noopener noreferrer",
|
|
28
29
|
target: "_blank",
|
|
29
30
|
},
|
|
30
|
-
}),
|
|
31
|
+
}),
|
|
31
32
|
];
|
|
32
33
|
|
|
33
34
|
const validateUrl = (value, allowEmpty = false) => {
|
|
@@ -38,48 +39,68 @@ const validateUrl = (value, allowEmpty = false) => {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
// URLs con espacios no son válidas
|
|
42
42
|
if (value.includes(" ")) {
|
|
43
43
|
return { valid: false, error: "markdown.editor.invalidUrl" };
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Regex más estricto: requiere dominio.tld válido
|
|
47
|
-
// - Dominio: al menos 2 caracteres antes del último punto
|
|
48
|
-
// - TLD: 2-6 letras
|
|
49
46
|
const parts = value.split(".");
|
|
50
47
|
if (parts.length < 2) {
|
|
51
48
|
return { valid: false, error: "markdown.editor.invalidUrl" };
|
|
52
49
|
}
|
|
53
|
-
|
|
50
|
+
|
|
54
51
|
const tld = parts[parts.length - 1].split("/")[0];
|
|
55
52
|
const domain = parts.slice(0, -1).join(".");
|
|
56
|
-
|
|
57
|
-
// Validar TLD: 2-6 letras
|
|
53
|
+
|
|
58
54
|
if (!/^[a-z]{2,6}$/i.test(tld)) {
|
|
59
55
|
return { valid: false, error: "markdown.editor.invalidUrl" };
|
|
60
56
|
}
|
|
61
|
-
|
|
62
|
-
// Validar dominio: al menos 4 caracteres si el TLD tiene 3 letras o menos
|
|
63
|
-
// Esto evita www.peo, www.xyz, etc. pero permite example.com
|
|
57
|
+
|
|
64
58
|
if (tld.length <= 3 && domain.length < 4) {
|
|
65
59
|
return { valid: false, error: "markdown.editor.invalidUrl" };
|
|
66
60
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
domain.length < 2 ||
|
|
64
|
+
domain.startsWith("-") ||
|
|
65
|
+
domain.endsWith("-") ||
|
|
66
|
+
domain.endsWith(".")
|
|
67
|
+
) {
|
|
70
68
|
return { valid: false, error: "markdown.editor.invalidUrl" };
|
|
71
69
|
}
|
|
72
70
|
|
|
73
|
-
const urlWithProtocol = value.match(/^https?:\/\//i)
|
|
71
|
+
const urlWithProtocol = value.match(/^https?:\/\//i)
|
|
72
|
+
? value
|
|
73
|
+
: `https://${value}`;
|
|
74
|
+
|
|
74
75
|
return { valid: true, value: urlWithProtocol };
|
|
75
76
|
};
|
|
76
77
|
|
|
78
|
+
function stripOuterCodeFence(text) {
|
|
79
|
+
const match = text.match(/^\s*```(?:markdown)?\s*\n([\s\S]*?)\n\s*```\s*$/);
|
|
80
|
+
return match ? match[1] : text;
|
|
81
|
+
}
|
|
82
|
+
|
|
77
83
|
function markdownToHtml(markdown) {
|
|
78
84
|
if (!markdown || (typeof markdown === "string" && markdown.trim() === "")) {
|
|
79
85
|
return "<p></p>";
|
|
80
86
|
}
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
|
|
88
|
+
const cleaned = stripOuterCodeFence(markdown);
|
|
89
|
+
|
|
90
|
+
const withLinks = marked
|
|
91
|
+
.parse(cleaned, { async: false })
|
|
92
|
+
.replace(
|
|
93
|
+
/<a (?=[^>]*href=)/gi,
|
|
94
|
+
`<a target="_blank" rel="noopener noreferrer" `,
|
|
95
|
+
)
|
|
96
|
+
.replace(
|
|
97
|
+
/href="(?!https?:\/\/|mailto:)([^"]+)"/gi,
|
|
98
|
+
(match, url) => `href="https://${url}"`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const safeHtml = DOMPurify.sanitize(withLinks, { ADD_ATTR: ["target"] });
|
|
102
|
+
|
|
103
|
+
return safeHtml;
|
|
83
104
|
}
|
|
84
105
|
|
|
85
106
|
function htmlToMarkdown(html) {
|
|
@@ -195,44 +216,55 @@ export function MarkdownEditor({
|
|
|
195
216
|
};
|
|
196
217
|
}, [editor]);
|
|
197
218
|
|
|
198
|
-
const openLinkModal = useCallback(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (url !== null) {
|
|
202
|
-
linkUrl = url.replace(/^https?:\/\//i, "");
|
|
203
|
-
} else {
|
|
219
|
+
const openLinkModal = useCallback(
|
|
220
|
+
(url = null) => {
|
|
204
221
|
const previousUrl = editor.getAttributes("link").href || "";
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
const tempUrl = url !== null ? url : previousUrl;
|
|
223
|
+
const linkUrl = tempUrl.replace(/^https?:\/\//i, "");
|
|
207
224
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
225
|
+
setLinkUrl(linkUrl);
|
|
226
|
+
setLinkModalOpen(true);
|
|
227
|
+
},
|
|
228
|
+
[editor],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const handleLinkAccept = useCallback(
|
|
232
|
+
({ valid, value: url }) => {
|
|
233
|
+
if (!valid) return;
|
|
234
|
+
|
|
235
|
+
if (!url || url.trim() === "") {
|
|
236
|
+
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
|
237
|
+
setLinkModalOpen(false);
|
|
238
|
+
setLinkUrl("");
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
211
241
|
|
|
212
|
-
|
|
213
|
-
if (!valid) return;
|
|
242
|
+
const finalUrl = url.startsWith("http") ? url : `https://${url}`;
|
|
214
243
|
|
|
215
|
-
|
|
216
|
-
|
|
244
|
+
editor
|
|
245
|
+
.chain()
|
|
246
|
+
.focus()
|
|
247
|
+
.extendMarkRange("link")
|
|
248
|
+
.setLink({ href: finalUrl })
|
|
249
|
+
.run();
|
|
217
250
|
setLinkModalOpen(false);
|
|
218
251
|
setLinkUrl("");
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
223
|
-
setLinkModalOpen(false);
|
|
224
|
-
setLinkUrl("");
|
|
225
|
-
}, [editor]);
|
|
252
|
+
},
|
|
253
|
+
[editor],
|
|
254
|
+
);
|
|
226
255
|
|
|
227
256
|
const handleLinkCancel = useCallback(() => {
|
|
228
257
|
setLinkModalOpen(false);
|
|
229
258
|
setLinkUrl("");
|
|
230
259
|
}, []);
|
|
231
260
|
|
|
232
|
-
const validateLinkUrl = useCallback(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
261
|
+
const validateLinkUrl = useCallback(
|
|
262
|
+
(value) => {
|
|
263
|
+
const hasPreviousLink = editor?.isActive("link");
|
|
264
|
+
return validateUrl(value, hasPreviousLink);
|
|
265
|
+
},
|
|
266
|
+
[editor],
|
|
267
|
+
);
|
|
236
268
|
|
|
237
269
|
if (!editor) {
|
|
238
270
|
return null;
|
|
@@ -327,7 +359,10 @@ export function MarkdownEditor({
|
|
|
327
359
|
<FormattedMessage id="markdown.editor.insertLink" />
|
|
328
360
|
</>
|
|
329
361
|
}
|
|
330
|
-
label={formatMessage({
|
|
362
|
+
label={formatMessage({
|
|
363
|
+
id: "markdown.editor.urlLabel",
|
|
364
|
+
defaultMessage: "URL",
|
|
365
|
+
})}
|
|
331
366
|
placeholder="https://"
|
|
332
367
|
/>
|
|
333
368
|
</div>
|