@truedat/core 8.3.5 → 8.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/core",
3
- "version": "8.3.5",
3
+ "version": "8.4.1",
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.5",
54
+ "@truedat/test": "8.4.1",
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": "12d1969c3403861d360fbdc24fd270ab1fece070"
100
+ "gitHead": "fc56d67c36dfe2d5db692b26ebdd2071b7518e1f"
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
 
@@ -74,12 +75,20 @@ const validateUrl = (value, allowEmpty = false) => {
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
- return marked
82
- .parse(markdown, { async: false })
87
+
88
+ const cleaned = stripOuterCodeFence(markdown);
89
+
90
+ const withLinks = marked
91
+ .parse(cleaned, { async: false })
83
92
  .replace(
84
93
  /<a (?=[^>]*href=)/gi,
85
94
  `<a target="_blank" rel="noopener noreferrer" `,
@@ -88,6 +97,10 @@ function markdownToHtml(markdown) {
88
97
  /href="(?!https?:\/\/|mailto:)([^"]+)"/gi,
89
98
  (match, url) => `href="https://${url}"`,
90
99
  );
100
+
101
+ const safeHtml = DOMPurify.sanitize(withLinks, { ADD_ATTR: ["target"] });
102
+
103
+ return safeHtml;
91
104
  }
92
105
 
93
106
  function htmlToMarkdown(html) {
@@ -106,6 +106,21 @@ describe("<MarkdownReader />", () => {
106
106
  );
107
107
  expect(rendered.container).toMatchSnapshot();
108
108
  });
109
+
110
+ it("renders MarkdownReader headings and links", () => {
111
+ const content = "## Title\n\n[link](https://example.com)";
112
+ const { container } = render(<MarkdownReader content={content} />);
113
+ const reader = container.querySelector(".markdown-reader");
114
+ expect(reader).toBeInTheDocument();
115
+
116
+ const anchor = container.querySelector("a");
117
+ expect(anchor).toBeInTheDocument();
118
+ expect(anchor.getAttribute("href")).toBe("https://example.com");
119
+ expect(anchor.getAttribute("target")).toBe("_blank");
120
+ expect(anchor.getAttribute("rel")).toBe("noopener noreferrer");
121
+
122
+ expect(reader.innerHTML).toBe(markdownToHtml(content));
123
+ });
109
124
  });
110
125
 
111
126
  describe("<MarkdownEditor />", () => {
@@ -137,4 +152,17 @@ describe("<MarkdownEditor />", () => {
137
152
  const editable = rendered.container.querySelector(".tiptap");
138
153
  expect(editable).toBeInTheDocument();
139
154
  });
155
+
156
+ it("renders MarkdownEditor formatted HTML from markdown value", async () => {
157
+ const value = "**Bold** text";
158
+ const { container } = render(<MarkdownEditor value={value} />);
159
+
160
+ await waitFor(() => {
161
+ const editable = container.querySelector(".tiptap");
162
+ expect(editable).toBeInTheDocument();
163
+ });
164
+
165
+ const tiptap = container.querySelector(".tiptap");
166
+ expect(tiptap.innerHTML).toBe("<p><strong>Bold</strong> text</p>");
167
+ });
140
168
  });
@@ -152,6 +152,7 @@ describe("services: i18nContent", () => {
152
152
  it("should identify translatable fields", () => {
153
153
  expect(isTranslatableField({ widget: "string" })).toBe(true);
154
154
  expect(isTranslatableField({ widget: "enriched_text" })).toBe(true);
155
+ expect(isTranslatableField({ widget: "markdown" })).toBe(true);
155
156
  expect(isTranslatableField({ widget: "textarea" })).toBe(true);
156
157
  });
157
158
 
@@ -1,6 +1,6 @@
1
1
  import _ from "lodash/fp";
2
2
 
3
- const translatableFieldWidgets = ["enriched_text", "string", "textarea"];
3
+ const translatableFieldWidgets = ["enriched_text", "markdown", "string", "textarea"];
4
4
 
5
5
  export const splitTranslatableFields = (template) =>
6
6
  _.flow(
@@ -21,6 +21,10 @@ export const splitTranslatableFields = (template) =>
21
21
  (w) => w == "enriched_text",
22
22
  _.constant({ value: {}, origin: "user" }),
23
23
  ],
24
+ [
25
+ (w) => w == "markdown",
26
+ _.constant({ value: "", origin: "user" }),
27
+ ],
24
28
  [
25
29
  (w) => w == "string",
26
30
  _.constant({ value: "", origin: "user" }),