@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.3.4",
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.4",
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": "6c7f4ef08bac69f8b642fa788ebb426a5a103314"
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
- // Validar dominio: al menos 2 caracteres, sin empezar/terminar con punto o guión
69
- if (domain.length < 2 || domain.startsWith("-") || domain.endsWith("-") || domain.endsWith(".")) {
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) ? value : `https://${value}`;
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
- const html = marked.parse(markdown, { async: false });
82
- return html.replace(/<a (?=[^>]*href=)/gi, `<a target="_blank" rel="noopener noreferrer" `);
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((url = null) => {
199
- let linkUrl;
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
- linkUrl = previousUrl.replace(/^https?:\/\//i, "");
206
- }
222
+ const tempUrl = url !== null ? url : previousUrl;
223
+ const linkUrl = tempUrl.replace(/^https?:\/\//i, "");
207
224
 
208
- setLinkUrl(linkUrl);
209
- setLinkModalOpen(true);
210
- }, [editor]);
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
- const handleLinkAccept = useCallback(({ valid, value: url }) => {
213
- if (!valid) return;
242
+ const finalUrl = url.startsWith("http") ? url : `https://${url}`;
214
243
 
215
- if (!url || url.trim() === "") {
216
- editor.chain().focus().extendMarkRange("link").unsetLink().run();
244
+ editor
245
+ .chain()
246
+ .focus()
247
+ .extendMarkRange("link")
248
+ .setLink({ href: finalUrl })
249
+ .run();
217
250
  setLinkModalOpen(false);
218
251
  setLinkUrl("");
219
- return;
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((value) => {
233
- const hasPreviousLink = editor?.isActive("link");
234
- return validateUrl(value, hasPreviousLink);
235
- }, [editor]);
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({ id: "markdown.editor.urlLabel", defaultMessage: "URL" })}
362
+ label={formatMessage({
363
+ id: "markdown.editor.urlLabel",
364
+ defaultMessage: "URL",
365
+ })}
331
366
  placeholder="https://"
332
367
  />
333
368
  </div>