@katechat/ui 1.0.1 → 1.0.3

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.
Files changed (58) hide show
  1. package/dist/cjs/index.css +491 -0
  2. package/dist/cjs/index.css.map +7 -0
  3. package/dist/cjs/index.js +75305 -0
  4. package/dist/cjs/index.js.map +7 -0
  5. package/dist/esm/index.css +491 -0
  6. package/dist/esm/index.css.map +7 -0
  7. package/dist/esm/index.js +75304 -0
  8. package/dist/esm/index.js.map +7 -0
  9. package/dist/index.css +1 -0
  10. package/dist/index.js +539 -0
  11. package/dist/types/tsconfig.tsbuildinfo +1 -0
  12. package/package.json +39 -3
  13. package/.prettierrc +0 -9
  14. package/esbuild.js +0 -56
  15. package/jest.config.js +0 -24
  16. package/postcss.config.cjs +0 -14
  17. package/src/__mocks__/fileMock.js +0 -1
  18. package/src/__mocks__/styleMock.js +0 -1
  19. package/src/components/chat/ChatMessagesContainer.module.scss +0 -77
  20. package/src/components/chat/ChatMessagesContainer.tsx +0 -151
  21. package/src/components/chat/ChatMessagesList.tsx +0 -216
  22. package/src/components/chat/index.ts +0 -4
  23. package/src/components/chat/input/ChatInput.module.scss +0 -113
  24. package/src/components/chat/input/ChatInput.tsx +0 -259
  25. package/src/components/chat/input/index.ts +0 -1
  26. package/src/components/chat/message/ChatMessage.Carousel.module.scss +0 -7
  27. package/src/components/chat/message/ChatMessage.module.scss +0 -378
  28. package/src/components/chat/message/ChatMessage.tsx +0 -271
  29. package/src/components/chat/message/ChatMessagePreview.tsx +0 -22
  30. package/src/components/chat/message/LinkedChatMessage.tsx +0 -64
  31. package/src/components/chat/message/MessageStatus.tsx +0 -38
  32. package/src/components/chat/message/controls/CopyMessageButton.tsx +0 -32
  33. package/src/components/chat/message/index.ts +0 -4
  34. package/src/components/icons/ProviderIcon.tsx +0 -49
  35. package/src/components/icons/index.ts +0 -1
  36. package/src/components/index.ts +0 -3
  37. package/src/components/modal/ImagePopup.tsx +0 -97
  38. package/src/components/modal/index.ts +0 -1
  39. package/src/controls/FileDropzone/FileDropzone.module.scss +0 -15
  40. package/src/controls/FileDropzone/FileDropzone.tsx +0 -120
  41. package/src/controls/index.ts +0 -1
  42. package/src/core/ai.ts +0 -1
  43. package/src/core/index.ts +0 -4
  44. package/src/core/message.ts +0 -59
  45. package/src/core/model.ts +0 -23
  46. package/src/core/user.ts +0 -8
  47. package/src/hooks/index.ts +0 -2
  48. package/src/hooks/useIntersectionObserver.ts +0 -24
  49. package/src/hooks/useTheme.tsx +0 -66
  50. package/src/index.ts +0 -5
  51. package/src/lib/__tests__/markdown.parser.test.ts +0 -289
  52. package/src/lib/__tests__/markdown.parser.testUtils.ts +0 -31
  53. package/src/lib/__tests__/markdown.parser_sanitizeUrl.test.ts +0 -130
  54. package/src/lib/assert.ts +0 -14
  55. package/src/lib/markdown.parser.ts +0 -189
  56. package/src/setupTests.ts +0 -1
  57. package/src/types/scss.d.ts +0 -4
  58. package/tsconfig.json +0 -26
@@ -1,289 +0,0 @@
1
- import { parseMarkdown, parseChatMessages, escapeHtml } from "../markdown.parser";
2
- import { Message, MessageRole } from "../../core/message";
3
-
4
- // Mock the sanitizeUrl function by accessing it through module internals
5
- // Since sanitizeUrl is not exported, we'll test it indirectly through parseMarkdown
6
- // or we can use jest.mock to access internals, but for now we'll test through integration
7
-
8
- describe("MarkdownParser", () => {
9
- describe("escapeHtml", () => {
10
- it("should escape HTML entities", () => {
11
- expect(escapeHtml('<script>alert("xss")</script>')).toBe("&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;");
12
- expect(escapeHtml("&<>\"'`")).toBe("&amp;&lt;&gt;&quot;&#x27;`");
13
- });
14
-
15
- it("should handle null and undefined", () => {
16
- expect(escapeHtml(null)).toBe("");
17
- expect(escapeHtml(undefined)).toBe("");
18
- expect(escapeHtml("")).toBe("");
19
- });
20
-
21
- it("should preserve safe text", () => {
22
- expect(escapeHtml("Hello World!")).toBe("Hello World!");
23
- expect(escapeHtml("123 + 456 = 579")).toBe("123 + 456 = 579");
24
- });
25
- });
26
-
27
- describe("parseMarkdown", () => {
28
- it("should handle null and undefined content", () => {
29
- expect(parseMarkdown(null)).toEqual([]);
30
- expect(parseMarkdown(undefined)).toEqual([]);
31
- expect(parseMarkdown("")).toEqual([]);
32
- });
33
-
34
- it("should parse simple markdown text", () => {
35
- const result = parseMarkdown("Hello **world**!");
36
- expect(result).toHaveLength(1);
37
- expect(result[0]).toContain("<strong>world</strong>");
38
- });
39
-
40
- it("should handle code blocks as single block", () => {
41
- const codeBlock = '```javascript\nconsole.log("hello");\n```';
42
- const result = parseMarkdown(codeBlock);
43
- expect(result).toHaveLength(1);
44
- expect(result[0]).toContain("console.log");
45
- });
46
-
47
- it("should handle tables as single block", () => {
48
- const table = "| Header 1 | Header 2 |\n|----------|----------|\n| Cell 1 | Cell 2 |";
49
- const result = parseMarkdown(table);
50
- expect(result).toHaveLength(1);
51
- expect(result[0]).toContain("<table>");
52
- });
53
-
54
- it("should split text by double newlines", () => {
55
- const text = "Paragraph 1\n\nParagraph 2\n\nParagraph 3";
56
- const result = parseMarkdown(text);
57
- expect(result).toHaveLength(3);
58
- expect(result[0]).toContain("Paragraph 1");
59
- expect(result[1]).toContain("Paragraph 2");
60
- expect(result[2]).toContain("Paragraph 3");
61
- });
62
-
63
- describe("Link Security Tests", () => {
64
- it("should sanitize javascript: URLs", () => {
65
- const maliciousLink = '[Click me](javascript:alert("xss"))';
66
- const result = parseMarkdown(maliciousLink);
67
- expect(result[0]).not.toContain("javascript:");
68
- // Should render as text or empty href
69
- expect(result[0]).toContain("Click me");
70
- });
71
-
72
- it("should sanitize data: URLs", () => {
73
- const dataUrl = '[Click me](data:text/html,<script>alert("xss")</script>)';
74
- const result = parseMarkdown(dataUrl);
75
- expect(result[0]).not.toContain("data:text/html");
76
- expect(result[0]).toContain("Click me");
77
- });
78
-
79
- it("should allow https URLs", () => {
80
- const httpsLink = "[Safe link](https://example.com)";
81
- const result = parseMarkdown(httpsLink);
82
- expect(result[0]).toContain('href="https://example.com"');
83
- expect(result[0]).toContain('target="_blank"');
84
- expect(result[0]).toContain('rel="noopener noreferrer"');
85
- });
86
-
87
- it("should allow http URLs", () => {
88
- const httpLink = "[HTTP link](http://example.com)";
89
- const result = parseMarkdown(httpLink);
90
- expect(result[0]).toContain('href="http://example.com"');
91
- });
92
-
93
- it("should allow mailto URLs", () => {
94
- const mailtoLink = "[Email me](mailto:test@example.com)";
95
- const result = parseMarkdown(mailtoLink);
96
- expect(result[0]).toContain('href="mailto:test@example.com"');
97
- });
98
-
99
- it("should handle protocol-relative URLs", () => {
100
- const protocolRelative = "[Link](//example.com)";
101
- const result = parseMarkdown(protocolRelative);
102
- expect(result[0]).toContain('href="https://example.com"');
103
- });
104
-
105
- it("should allow relative URLs", () => {
106
- const relativeLink = "[Relative](/path/to/page)";
107
- const result = parseMarkdown(relativeLink);
108
- expect(result[0]).toContain('href="/path/to/page"');
109
- });
110
-
111
- it("should escape link titles and text", () => {
112
- const linkWithQuotes = '[Link with "quotes"](https://example.com "Title with \'quotes\'")';
113
- const result = parseMarkdown(linkWithQuotes);
114
- expect(result[0]).toContain('title="Title with &#x27;quotes&#x27;"');
115
- expect(result[0]).toContain("Link with &quot;quotes&quot;");
116
- });
117
-
118
- it("should handle XSS attempts in link text", () => {
119
- const xssText = '[<script>alert("xss")</script>](https://example.com)';
120
- const result = parseMarkdown(xssText);
121
- expect(result[0]).not.toContain("<script>");
122
- expect(result[0]).toContain("&lt;script&gt;");
123
- });
124
-
125
- it("should handle XSS attempts in link titles", () => {
126
- const xssTitle = '[Link](https://example.com "Title<script>alert(\\"xss\\")</script>")';
127
- const result = parseMarkdown(xssTitle);
128
- expect(result[0]).not.toContain("<script>");
129
- expect(result[0]).toContain("&lt;script&gt;");
130
- });
131
- });
132
-
133
- describe("Math Formula Tests", () => {
134
- it("should render inline math formulas", () => {
135
- const mathFormula = "The equation is $x^2 + y^2 = z^2$";
136
- const result = parseMarkdown(mathFormula);
137
- expect(result[0]).toContain("katex");
138
- });
139
-
140
- it("should render block math formulas", () => {
141
- const blockMath = "$$\\sum_{i=1}^{n} x_i$$";
142
- const result = parseMarkdown(blockMath);
143
- expect(result[0]).toContain("katex");
144
- });
145
- });
146
-
147
- describe("Code Highlighting Tests", () => {
148
- it("should highlight code with language specified", () => {
149
- const code = "```javascript\nconst x = 5;\n```";
150
- const result = parseMarkdown(code);
151
- expect(result[0]).toContain("hljs");
152
- expect(result[0]).toContain("code-data");
153
- });
154
-
155
- it("should handle code without language", () => {
156
- const code = "```\nsome code\n```";
157
- const result = parseMarkdown(code);
158
- // Code blocks without language don't get hljs classes applied
159
- expect(result[0]).toContain("<pre><code>");
160
- expect(result[0]).toContain("some code");
161
- });
162
- });
163
- });
164
-
165
- describe("parseChatMessages", () => {
166
- const mockMessage: Message = {
167
- id: "1",
168
- chatId: "chat1",
169
- content: "Hello **world**!",
170
- role: MessageRole.USER,
171
- createdAt: new Date().toISOString(),
172
- updatedAt: new Date().toISOString(),
173
- };
174
-
175
- it("should handle empty messages array", () => {
176
- expect(parseChatMessages([])).toEqual([]);
177
- expect(parseChatMessages()).toEqual([]);
178
- });
179
-
180
- it("should parse USER and ASSISTANT messages as markdown", () => {
181
- const messages: Message[] = [
182
- { ...mockMessage, role: MessageRole.USER },
183
- { ...mockMessage, role: MessageRole.ASSISTANT, id: "2" },
184
- ];
185
-
186
- const result = parseChatMessages(messages);
187
-
188
- expect(result).toHaveLength(2);
189
- expect(result[0].html).toBeDefined();
190
- expect(result[1].html).toBeDefined();
191
- expect(result[0].html![0]).toContain("<strong>world</strong>");
192
- expect(result[1].html![0]).toContain("<strong>world</strong>");
193
- });
194
-
195
- it("should escape non-USER/ASSISTANT messages", () => {
196
- const systemMessage: Message = {
197
- ...mockMessage,
198
- role: MessageRole.ERROR,
199
- content: '<script>alert("xss")</script>',
200
- };
201
-
202
- const result = parseChatMessages([systemMessage]);
203
-
204
- expect(result).toHaveLength(1);
205
- expect(result[0].html![0]).toBe("&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;");
206
- });
207
-
208
- it("should handle linked messages", () => {
209
- const messageWithLinked: Message = {
210
- ...mockMessage,
211
- linkedMessages: [
212
- { ...mockMessage, id: "2", role: MessageRole.ASSISTANT, content: "Response **bold**" },
213
- { ...mockMessage, id: "3", role: MessageRole.SYSTEM, content: '<script>alert("xss")</script>' },
214
- ],
215
- };
216
-
217
- const result = parseChatMessages([messageWithLinked]);
218
-
219
- expect(result).toHaveLength(1);
220
- expect(result[0].linkedMessages).toHaveLength(2);
221
- expect(result[0].linkedMessages![0].html![0]).toContain("<strong>bold</strong>");
222
- expect(result[0].linkedMessages![1].html![0]).toContain("&lt;script&gt;");
223
- });
224
-
225
- it("should handle malicious links in chat messages", () => {
226
- const maliciousMessage: Message = {
227
- ...mockMessage,
228
- content: '[Click here](javascript:alert("xss")) for a "surprise"',
229
- role: MessageRole.USER,
230
- };
231
-
232
- const result = parseChatMessages([maliciousMessage]);
233
-
234
- expect(result[0].html![0]).not.toContain("javascript:");
235
- expect(result[0].html![0]).toContain("Click here");
236
- expect(result[0].html![0]).toContain("&quot;surprise&quot;");
237
- });
238
-
239
- it("should preserve message metadata", () => {
240
- const result = parseChatMessages([mockMessage]);
241
-
242
- expect(result[0].id).toBe(mockMessage.id);
243
- expect(result[0].role).toBe(mockMessage.role);
244
- expect(result[0].createdAt).toBe(mockMessage.createdAt);
245
- expect(result[0].updatedAt).toBe(mockMessage.updatedAt);
246
- });
247
- });
248
-
249
- describe("HTML Injection Prevention", () => {
250
- it("should prevent HTML injection in regular text", () => {
251
- const maliciousText = 'Hello <img src="x" onerror="alert(\'xss\')">';
252
- const result = parseMarkdown(maliciousText);
253
- // HTML should be escaped, but onerror= might still appear in escaped form
254
- expect(result[0]).toContain("&lt;img");
255
- expect(result[0]).toContain("&quot;alert");
256
- // Should not contain unescaped HTML
257
- expect(result[0]).not.toContain('<img src="x"');
258
- });
259
-
260
- it("should prevent HTML injection in code blocks", () => {
261
- const maliciousCode = '```html\n<script>alert("xss")</script>\n```';
262
- const result = parseMarkdown(maliciousCode);
263
- // Code in code blocks should be syntax highlighted but not executed
264
- // HTML syntax highlighting will show tags but they're still safe
265
- expect(result[0]).toContain("hljs-tag");
266
- expect(result[0]).toContain("script");
267
- // Should not contain executable script tag
268
- expect(result[0]).not.toContain('<script>alert("xss")</script>');
269
- });
270
-
271
- it("should handle mixed content safely", () => {
272
- const mixedContent = `
273
- # Header with <script>alert("xss")</script>
274
-
275
- [Malicious link](javascript:void(0)) and normal [safe link](https://example.com)
276
-
277
- \`\`\`javascript
278
- // This <script> should be highlighted but not executed
279
- console.log("<script>alert('safe')</script>");
280
- \`\`\`
281
- `;
282
-
283
- const result = parseMarkdown(mixedContent);
284
- expect(result[0]).not.toContain("javascript:void(0)");
285
- expect(result[0]).toContain("https://example.com");
286
- expect(result[0]).toContain("&lt;script&gt;alert(&#x27;safe&#x27;)&lt;/script&gt;");
287
- });
288
- });
289
- });
@@ -1,31 +0,0 @@
1
- // Test utilities to access internal functions
2
- // This file helps test non-exported functions from MarkdownParser
3
-
4
- import { parseMarkdown } from "../markdown.parser";
5
-
6
- /**
7
- * Test the sanitizeUrl function indirectly by checking link rendering
8
- */
9
- export function testSanitizeUrl(url: string): string {
10
- const testMarkdown = `[test](${url})`;
11
- const result = parseMarkdown(testMarkdown);
12
-
13
- // Extract href value from the rendered HTML
14
- const match = result[0].match(/href="([^"]*)"/);
15
- return match ? match[1] : "";
16
- }
17
-
18
- /**
19
- * Test if a URL gets blocked (returns empty href)
20
- */
21
- export function testUrlBlocked(url: string): boolean {
22
- return testSanitizeUrl(url) === "";
23
- }
24
-
25
- /**
26
- * Test if a URL gets allowed and properly escaped
27
- */
28
- export function testUrlAllowed(url: string, expectedUrl?: string): boolean {
29
- const result = testSanitizeUrl(url);
30
- return result !== "" && (expectedUrl ? result === expectedUrl : true);
31
- }
@@ -1,130 +0,0 @@
1
- import { testSanitizeUrl, testUrlBlocked, testUrlAllowed } from "./markdown.parser.testUtils";
2
-
3
- describe("sanitizeUrl (internal function tests)", () => {
4
- describe("Dangerous URL Blocking", () => {
5
- it("should block javascript: URLs", () => {
6
- expect(testUrlBlocked('javascript:alert("xss")')).toBe(true);
7
- expect(testUrlBlocked("JavaScript:void(0)")).toBe(true);
8
- expect(testUrlBlocked("JAVASCRIPT:alert(1)")).toBe(true);
9
- });
10
-
11
- it("should block data: URLs", () => {
12
- expect(testUrlBlocked('data:text/html,<script>alert("xss")</script>')).toBe(true);
13
- expect(testUrlBlocked("data:image/svg+xml,<svg onload=alert(1)>")).toBe(true);
14
- expect(testUrlBlocked("DATA:text/plain,malicious")).toBe(true);
15
- });
16
-
17
- it("should block other dangerous protocols", () => {
18
- expect(testUrlBlocked('vbscript:msgbox("xss")')).toBe(true);
19
- expect(testUrlBlocked("file:///etc/passwd")).toBe(true);
20
- expect(testUrlBlocked("ftp://malicious.com")).toBe(true);
21
- });
22
-
23
- it("should handle empty and null-like values", () => {
24
- expect(testSanitizeUrl("")).toBe("");
25
- expect(testSanitizeUrl(" ")).toBe("");
26
- });
27
- });
28
-
29
- describe("Safe URL Allowing", () => {
30
- it("should allow https URLs", () => {
31
- expect(testUrlAllowed("https://example.com")).toBe(true);
32
- expect(testSanitizeUrl("https://example.com")).toBe("https://example.com");
33
- });
34
-
35
- it("should allow http URLs", () => {
36
- expect(testUrlAllowed("http://example.com")).toBe(true);
37
- expect(testSanitizeUrl("http://example.com")).toBe("http://example.com");
38
- });
39
-
40
- it("should allow mailto URLs", () => {
41
- expect(testUrlAllowed("mailto:test@example.com")).toBe(true);
42
- expect(testSanitizeUrl("mailto:test@example.com")).toBe("mailto:test@example.com");
43
- });
44
-
45
- it("should handle protocol-relative URLs", () => {
46
- expect(testSanitizeUrl("//example.com")).toBe("https://example.com");
47
- expect(testSanitizeUrl("//cdn.example.com/file.css")).toBe("https://cdn.example.com/file.css");
48
- });
49
-
50
- it("should allow relative URLs", () => {
51
- expect(testUrlAllowed("/path/to/page")).toBe(true);
52
- expect(testUrlAllowed("./relative/path")).toBe(true);
53
- expect(testUrlAllowed("../parent/path")).toBe(true);
54
- expect(testSanitizeUrl("/api/endpoint")).toBe("/api/endpoint");
55
- });
56
- });
57
-
58
- describe("URL Escaping", () => {
59
- it("should escape HTML entities in URLs", () => {
60
- const urlWithEntities = "https://example.com/search?q=<script>&amp=value";
61
- const result = testSanitizeUrl(urlWithEntities);
62
- expect(result).toContain("&lt;script&gt;");
63
- expect(result).toContain("&amp;amp=value");
64
- });
65
-
66
- it("should handle URLs with quotes", () => {
67
- const urlWithQuotes = "https://example.com/path?param=\"value\"&other='test'";
68
- const result = testSanitizeUrl(urlWithQuotes);
69
- expect(result).toContain("&quot;value&quot;");
70
- expect(result).toContain("&#x27;test&#x27;");
71
- });
72
-
73
- it("should preserve query parameters safely", () => {
74
- const complexUrl = "https://api.example.com/search?q=test&sort=date&filter[]=category";
75
- const result = testSanitizeUrl(complexUrl);
76
- // Ampersands get escaped as expected
77
- expect(result).toBe("https://api.example.com/search?q=test&amp;sort=date&amp;filter[]=category");
78
- });
79
- });
80
-
81
- describe("Edge Cases", () => {
82
- it("should handle URLs with whitespace", () => {
83
- expect(testSanitizeUrl(" https://example.com ")).toBe("https://example.com");
84
- // Note: literal \n characters in string, not actual newlines
85
- expect(testSanitizeUrl("https://example.com")).toBe("https://example.com");
86
- });
87
-
88
- it("should handle mixed case protocols", () => {
89
- expect(testSanitizeUrl("HTTPS://EXAMPLE.COM")).toBe("HTTPS://EXAMPLE.COM");
90
- expect(testSanitizeUrl("Http://example.com")).toBe("Http://example.com");
91
- expect(testSanitizeUrl("MailTo:test@example.com")).toBe("MailTo:test@example.com");
92
- });
93
-
94
- it("should handle URLs without protocols correctly", () => {
95
- expect(testUrlAllowed("example.com")).toBe(true);
96
- expect(testUrlAllowed("www.example.com/path")).toBe(true);
97
- expect(testSanitizeUrl("example.com")).toBe("example.com");
98
- });
99
-
100
- it("should handle domain names that contain suspicious words", () => {
101
- // These are actually valid domain names, just happen to contain suspicious words
102
- // They should be allowed as they're not actually protocols
103
- expect(testUrlAllowed("javascript.com:8080")).toBe(true);
104
- expect(testUrlAllowed("data.evil.com:3000")).toBe(true);
105
-
106
- // But actual protocols should still be blocked
107
- expect(testUrlBlocked("javascript:alert(1)")).toBe(true);
108
- expect(testUrlBlocked("data:text/html,<script>")).toBe(true);
109
- });
110
- });
111
-
112
- describe("Security Edge Cases", () => {
113
- it("should handle URL encoding attempts", () => {
114
- expect(testUrlBlocked("javascript%3Aalert(1)")).toBe(true);
115
- expect(testUrlBlocked("data%3Atext/html,<script>")).toBe(true);
116
- });
117
-
118
- it("should handle various protocol separators", () => {
119
- expect(testUrlBlocked("javascript://alert(1)")).toBe(true);
120
- expect(testUrlBlocked("javascript:\\\\alert(1)")).toBe(true);
121
- });
122
-
123
- it("should handle attempts to bypass with special characters", () => {
124
- // These specific attempts with literal strings may not be blocked by our simple regex
125
- // but the basic javascript: detection should work
126
- expect(testUrlBlocked("javascript:alert(1)")).toBe(true);
127
- expect(testUrlBlocked("JAVASCRIPT:alert(1)")).toBe(true);
128
- });
129
- });
130
- });
package/src/lib/assert.ts DELETED
@@ -1,14 +0,0 @@
1
- // Pure assertion tests whether a value is truthy, as determined by !!value.
2
- export function ok<T>(value: T, message?: string | Error): asserts value {
3
- if (!value) {
4
- if (message instanceof Error) {
5
- throw message;
6
- }
7
-
8
- throw new Error(message || "No value argument passed to `assert.ok()`");
9
- }
10
- }
11
-
12
- export function notEmpty<T>(value: T | undefined | null): value is T {
13
- return value != undefined;
14
- }
@@ -1,189 +0,0 @@
1
- import hljs from "highlight.js";
2
- import { Marked, Renderer } from "marked";
3
- import { markedHighlight } from "marked-highlight";
4
- import markedKatex from "marked-katex-extension";
5
-
6
- import { Message, MessageRole } from "@/core/message";
7
-
8
- // Template to store original (unformatted) code to copy it
9
- const CodeDataTemplate = `<span class="code-data" data-code="<CODE>" data-lang="<LANG>"></span>`;
10
-
11
- const marked = new Marked(
12
- // code highlighting
13
- markedHighlight({
14
- emptyLangClass: "hljs plaintext",
15
- langPrefix: "hljs ",
16
- highlight(code: string, lang: string) {
17
- const language = hljs.getLanguage(lang) ? lang : "plaintext";
18
- const formattedCode: string = hljs.highlight(code?.trim(), { language }).value;
19
-
20
- if (lang) {
21
- return (
22
- CodeDataTemplate.replaceAll("<LANG>", language).replaceAll("<CODE>", encodeURIComponent(code)) +
23
- formattedCode?.trim()
24
- );
25
- }
26
-
27
- return formattedCode;
28
- },
29
- }),
30
-
31
- // MatJAX formulas processing
32
- // example prompt with Claude V3 Haiku: "show me some example math equations like pythagoras, also add some example of an addition of 2 matrixes"
33
- // example prompt with Claude V3 Haiku: "show a proof of x is smaller y"
34
- markedKatex({
35
- displayMode: false,
36
- throwOnError: false,
37
- output: "html",
38
- }),
39
- { silent: true, gfm: true, breaks: true }
40
- );
41
-
42
- const markedSimple = new Marked(
43
- markedHighlight({
44
- emptyLangClass: "hljs plaintext",
45
- langPrefix: "hljs ",
46
- highlight(code: string, lang: string) {
47
- const language = hljs.getLanguage(lang) ? lang : "plaintext";
48
- return hljs.highlight(code?.trim(), { language }).value;
49
- },
50
- }),
51
- { silent: true, breaks: true, gfm: true }
52
- );
53
-
54
- const renderer = new Renderer();
55
- renderer.html = ({ text }: { text: string }) => {
56
- return escapeHtml(text);
57
- };
58
- renderer.link = ({ href, title, text }) => {
59
- // Sanitize URL to prevent XSS attacks
60
- const url = sanitizeUrl(href);
61
- return `<a target="_blank" rel="noopener noreferrer" href="${url}" title="${escapeHtml(title) || ""}">${escapeHtml(text)}</a>`;
62
- };
63
-
64
- export function normalizeMatJAX(input: string): string {
65
- return input
66
- ? input
67
- .replace(/\\\(([\s\S]+?)\\\)/g, (_, expr) => `$${expr}$`)
68
- // Block math: \[ ... \] → $$ ... $$ (on newlines for KaTeX block mode)
69
- .replace(/\\\[([\s\S]+?)\\\]/g, (_, expr) => `\n$$${expr}$$\n`)
70
- : "";
71
- }
72
-
73
- /**
74
- * Parse markdown to html blocks
75
- * @param content Raw markdown
76
- * @returns Array for formatted HTML blocks to be rendered
77
- */
78
- export function parseMarkdown(content?: string | null, simple = false): string[] {
79
- if (!content) return [];
80
-
81
- if (simple) {
82
- return [markedSimple.parse(content, { renderer }) as string];
83
- }
84
-
85
- content = normalizeMatJAX(content);
86
- // process complex code blocks, tables as one block
87
- if (content.match(/(```)|(\|---)/)) {
88
- return [marked.parse(content, { renderer }) as string];
89
- }
90
-
91
- // split large texts
92
- const parts = content
93
- .split(/(\r)?\n(\r)?\n/g)
94
- .filter(s => Boolean(s))
95
- .map(s => s + "\n\n");
96
-
97
- return parts.map(part => marked.parse(part, { renderer }) as string);
98
- }
99
-
100
- export function parseChatMessages(messages: Message[] = []): Message[] {
101
- const parsedMessages: Message[] = Array<Message>(messages.length);
102
- for (let i = 0; i < messages.length; i++) {
103
- const message = messages[i];
104
- const html =
105
- message.role === MessageRole.ASSISTANT || message.role === MessageRole.USER
106
- ? parseMarkdown(message.content)
107
- : [escapeHtml(message.content) || ""];
108
-
109
- let linkedMessages = message.linkedMessages;
110
- if (linkedMessages) {
111
- const linkedMessagesParsed = Array<Message>(linkedMessages.length);
112
- for (let ndx = 0; ndx < linkedMessages.length; ndx++) {
113
- const linkedMessage = linkedMessages[ndx];
114
- if (linkedMessage.role === MessageRole.ASSISTANT || linkedMessage.role === MessageRole.USER) {
115
- linkedMessagesParsed[ndx] = {
116
- ...linkedMessage,
117
- html: parseMarkdown(linkedMessage.content),
118
- };
119
- } else {
120
- linkedMessagesParsed[ndx] = {
121
- ...linkedMessage,
122
- html: [escapeHtml(linkedMessage.content) || ""],
123
- };
124
- }
125
- }
126
-
127
- linkedMessages = linkedMessagesParsed;
128
- }
129
-
130
- parsedMessages[i] = {
131
- ...message,
132
- linkedMessages,
133
- html,
134
- };
135
- }
136
-
137
- return parsedMessages;
138
- }
139
-
140
- const ESCAPE_HTML_ENTITIES: { [key: string]: string } = {
141
- "&": "&amp;",
142
- "<": "&lt;",
143
- ">": "&gt;",
144
- '"': "&quot;",
145
- "'": "&#x27;",
146
- };
147
-
148
- export function escapeHtml(text?: string | null): string {
149
- if (!text) return "";
150
- return text.replace(/[&<>"']/g, match => ESCAPE_HTML_ENTITIES[match] || match);
151
- }
152
-
153
- /**
154
- * Sanitize URL to prevent XSS attacks
155
- * Only allows http, https, and mailto protocols
156
- */
157
- function sanitizeUrl(url?: string | null): string {
158
- if (!url) return "";
159
-
160
- // Remove any whitespace and decode basic URL encoding for protocol detection
161
- const trimmedUrl = url.trim();
162
- const decodedUrl = decodeURIComponent(trimmedUrl).toLowerCase();
163
-
164
- const allowedProtocols = /^(https?:\/\/|mailto:)/i;
165
- if (allowedProtocols.test(trimmedUrl)) {
166
- return escapeHtml(trimmedUrl);
167
- }
168
-
169
- // If it starts with //, assume https
170
- if (trimmedUrl.startsWith("//")) {
171
- return escapeHtml(`https:${trimmedUrl}`);
172
- }
173
-
174
- const dangerousProtocols = /^(javascript|data|vbscript|file|ftp):/i;
175
- if (dangerousProtocols.test(decodedUrl)) {
176
- return "";
177
- }
178
- if (decodedUrl.includes("javascript:") || decodedUrl.includes("data:") || decodedUrl.includes("vbscript:")) {
179
- return "";
180
- }
181
-
182
- // If it looks like a relative path or doesn't have a protocol, allow it
183
- if (trimmedUrl.startsWith("/") || !trimmedUrl.includes("://")) {
184
- return escapeHtml(trimmedUrl);
185
- }
186
-
187
- // Block any other unknown protocols
188
- return "";
189
- }
package/src/setupTests.ts DELETED
@@ -1 +0,0 @@
1
- import "@testing-library/jest-dom";
@@ -1,4 +0,0 @@
1
- declare module "*.scss" {
2
- export const content: { [className: string]: string };
3
- export default content;
4
- }