@gitlab/duo-ui 8.16.1 → 8.17.0
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/CHANGELOG.md +15 -0
- package/dist/components/chat/markdown_renderer.js +22 -3
- package/dist/components/chat/mock_data.js +34 -2
- package/dist/index.js +1 -0
- package/package.json +2 -2
- package/src/components/chat/markdown_renderer.js +25 -13
- package/src/components/chat/mock_data.js +34 -3
- package/src/index.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [8.17.0](https://gitlab.com/gitlab-org/duo-ui/compare/v8.16.2...v8.17.0) (2025-05-29)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* export function to add markdown plugins ([dc36846](https://gitlab.com/gitlab-org/duo-ui/commit/dc368460e7bcee5476765846077529e27990d1a4))
|
|
7
|
+
|
|
8
|
+
## [8.16.2](https://gitlab.com/gitlab-org/duo-ui/compare/v8.16.1...v8.16.2) (2025-05-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* correct config option for DOMPurify ([5085b3c](https://gitlab.com/gitlab-org/duo-ui/commit/5085b3cc3e6c9e690aef7d2d35644076a9b7ea50))
|
|
14
|
+
* fixed nested code blocks ([1a1953a](https://gitlab.com/gitlab-org/duo-ui/commit/1a1953aab48ecaf72160be24f29f0be48ff79a7f))
|
|
15
|
+
|
|
1
16
|
## [8.16.1](https://gitlab.com/gitlab-org/duo-ui/compare/v8.16.0...v8.16.1) (2025-05-21)
|
|
2
17
|
|
|
3
18
|
|
|
@@ -9,7 +9,7 @@ const duoMarked = new Marked([{
|
|
|
9
9
|
gfm: false
|
|
10
10
|
}, markedBidi()]);
|
|
11
11
|
const config = {
|
|
12
|
-
|
|
12
|
+
ADD_TAGS: ['insert-code-snippet', 'copy-code', 'gl-markdown', '#text', 'gl-compact-markdown'],
|
|
13
13
|
ADD_ATTR: ['data-canonical-lang', 'data-sourcepos', 'lang', 'data-src', 'img'],
|
|
14
14
|
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'button'],
|
|
15
15
|
FORBID_ATTR: ['onerror', 'onload', 'onclick']
|
|
@@ -96,6 +96,22 @@ const sanitizeLinksHook = function () {
|
|
|
96
96
|
node.removeAttribute('href');
|
|
97
97
|
};
|
|
98
98
|
};
|
|
99
|
+
function isHtml(markup) {
|
|
100
|
+
const src = markup.toString().trim();
|
|
101
|
+
if (src.length === 0 || !src.startsWith('<') || !src.includes('>')) {
|
|
102
|
+
return false; // fast-fail trivial cases
|
|
103
|
+
}
|
|
104
|
+
const doc = new DOMParser().parseFromString(src, 'text/html');
|
|
105
|
+
|
|
106
|
+
// DOMParser drops invalid tags but inserts a <parsererror> element on failure
|
|
107
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling
|
|
108
|
+
if (doc.querySelector('parsererror') !== null) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Do we have at least one real element node inside <body>?
|
|
113
|
+
return Array.from(doc.body.childNodes).some(n => n.nodeType === Node.ELEMENT_NODE);
|
|
114
|
+
}
|
|
99
115
|
function renderDuoChatMarkdownPreview(md) {
|
|
100
116
|
let {
|
|
101
117
|
trustedUrls = []
|
|
@@ -103,11 +119,14 @@ function renderDuoChatMarkdownPreview(md) {
|
|
|
103
119
|
if (!md) return '';
|
|
104
120
|
DOMPurify.addHook('beforeSanitizeElements', handleImageElements);
|
|
105
121
|
DOMPurify.addHook('afterSanitizeAttributes', sanitizeLinksHook(trustedUrls));
|
|
106
|
-
const parsedMarkdown = duoMarked.parse(md.toString());
|
|
122
|
+
const parsedMarkdown = isHtml(md) ? md : duoMarked.parse(md.toString());
|
|
107
123
|
const sanitized = DOMPurify.sanitize(parsedMarkdown, config);
|
|
108
124
|
DOMPurify.removeHook('beforeSanitizeElements');
|
|
109
125
|
DOMPurify.removeHook('afterSanitizeAttributes');
|
|
110
126
|
return sanitized;
|
|
111
127
|
}
|
|
128
|
+
function addDuoMarkdownPlugin(plugin) {
|
|
129
|
+
duoMarked.use(plugin);
|
|
130
|
+
}
|
|
112
131
|
|
|
113
|
-
export { renderDuoChatMarkdownPreview };
|
|
132
|
+
export { addDuoMarkdownPlugin, renderDuoChatMarkdownPreview };
|
|
@@ -19,8 +19,40 @@ const MOCK_SOURCES = [{
|
|
|
19
19
|
}];
|
|
20
20
|
const MOCK_RESPONSE_MESSAGE = {
|
|
21
21
|
id: '123',
|
|
22
|
-
content: '
|
|
23
|
-
contentHtml:
|
|
22
|
+
content: 'I\'ll write a simple Python function with comments for you. Here\'s an example:\\\\n\\\\n```python\\\\ndef calculate_factorial(n):\\\\n \\\\\\"\\\\\\"\\\\\\"\\\\n Calculate the factorial of a non-negative integer.\\\\n \\\\n Args:\\\\n n (int): A non-negative integer\\\\n \\\\n Returns:\\\\n int: The factorial of n (n!)\\\\n \\\\n Examples:\\\\n >>> calculate_factorial(5)\\\\n 120\\\\n >>> calculate_factorial(0)\\\\n 1\\\\n \\\\\\"\\\\\\"\\\\\\"\\\\n # Handle base cases\\\\n if n == 0 or n == 1:\\\\n return 1\\\\n \\\\n # Use recursion to calculate factorial\\\\n return n * calculate_factorial(n - 1)\\\\n```\\\\n\\\\nThis function calculates the factorial of a number using recursion. It includes a docstring explaining what the function does, its parameters, return value, and usage examples, plus inline comments explaining the logic.',
|
|
23
|
+
contentHtml: `<p dir="auto">I'd be happy to write a simple Python function with comments for you. </p>
|
|
24
|
+
<p dir="auto">Here's a basic Python function that calculates the factorial of a number:</p>
|
|
25
|
+
<div class="gl-relative markdown-code-block js-markdown-code"><pre><code class="language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">factorial</span>(<span class="hljs-params">n</span>):
|
|
26
|
+
<span class="hljs-string">"""
|
|
27
|
+
Calculate the factorial of a non-negative integer.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
n (int): A non-negative integer
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
int: The factorial of n (n!)
|
|
34
|
+
|
|
35
|
+
Examples:
|
|
36
|
+
>>> factorial(5)
|
|
37
|
+
120
|
|
38
|
+
>>> factorial(0)
|
|
39
|
+
1
|
|
40
|
+
"""</span>
|
|
41
|
+
<span class="hljs-comment"># Base case: factorial of 0 or 1 is 1</span>
|
|
42
|
+
<span class="hljs-keyword">if</span> n <= <span class="hljs-number">1</span>:
|
|
43
|
+
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
|
|
44
|
+
|
|
45
|
+
<span class="hljs-comment"># Recursive case: n! = n * (n-1)!</span>
|
|
46
|
+
<span class="hljs-keyword">return</span> n * factorial(n - <span class="hljs-number">1</span>)
|
|
47
|
+
</code></pre>
|
|
48
|
+
<copy-code></copy-code><insert-code-snippet></insert-code-snippet></div><p dir="auto">This function includes:</p>
|
|
49
|
+
<ul dir="auto">
|
|
50
|
+
<li>A docstring explaining what the function does</li>
|
|
51
|
+
<li>Parameter and return type documentation</li>
|
|
52
|
+
<li>Usage examples</li>
|
|
53
|
+
<li>Inline comments explaining the logic</li>
|
|
54
|
+
</ul>
|
|
55
|
+
<p dir="auto">Is there a specific type of function you'd like me to create instead?</p>`,
|
|
24
56
|
role: MESSAGE_MODEL_ROLES.assistant,
|
|
25
57
|
extras: {
|
|
26
58
|
sources: MOCK_SOURCES,
|
package/dist/index.js
CHANGED
|
@@ -16,3 +16,4 @@ export { default as DuoChatContextItemDetailsModal } from './components/chat/com
|
|
|
16
16
|
export { default as DuoChatContextItemMenu } from './components/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu';
|
|
17
17
|
export { default as DuoChatContextItemPopover } from './components/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover';
|
|
18
18
|
export { default as DuoChatContextItemSelections } from './components/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections';
|
|
19
|
+
export { addDuoMarkdownPlugin } from './components/chat/markdown_renderer';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitlab/duo-ui",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.17.0",
|
|
4
4
|
"description": "Duo UI Components",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"@gitlab/eslint-plugin": "20.7.1",
|
|
101
101
|
"@gitlab/fonts": "^1.3.0",
|
|
102
102
|
"@gitlab/stylelint-config": "6.2.2",
|
|
103
|
-
"@gitlab/svgs": "^3.
|
|
103
|
+
"@gitlab/svgs": "^3.134.0",
|
|
104
104
|
"@gitlab/ui": "latest",
|
|
105
105
|
"@jest/test-sequencer": "^29.7.0",
|
|
106
106
|
"@rollup/plugin-commonjs": "^11.1.0",
|
|
@@ -13,18 +13,7 @@ const duoMarked = new Marked([
|
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
15
|
const config = {
|
|
16
|
-
|
|
17
|
-
'p',
|
|
18
|
-
'#text',
|
|
19
|
-
'div',
|
|
20
|
-
'code',
|
|
21
|
-
'insert-code-snippet',
|
|
22
|
-
'gl-markdown',
|
|
23
|
-
'pre',
|
|
24
|
-
'span',
|
|
25
|
-
'gl-compact-markdown',
|
|
26
|
-
'copy-code',
|
|
27
|
-
],
|
|
16
|
+
ADD_TAGS: ['insert-code-snippet', 'copy-code', 'gl-markdown', '#text', 'gl-compact-markdown'],
|
|
28
17
|
ADD_ATTR: ['data-canonical-lang', 'data-sourcepos', 'lang', 'data-src', 'img'],
|
|
29
18
|
FORBID_TAGS: ['script', 'style', 'iframe', 'form', 'button'],
|
|
30
19
|
FORBID_ATTR: ['onerror', 'onload', 'onclick'],
|
|
@@ -116,16 +105,39 @@ const sanitizeLinksHook =
|
|
|
116
105
|
node.removeAttribute('href');
|
|
117
106
|
};
|
|
118
107
|
|
|
108
|
+
function isHtml(markup) {
|
|
109
|
+
const src = markup.toString().trim();
|
|
110
|
+
if (src.length === 0 || !src.startsWith('<') || !src.includes('>')) {
|
|
111
|
+
return false; // fast-fail trivial cases
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const doc = new DOMParser().parseFromString(src, 'text/html');
|
|
115
|
+
|
|
116
|
+
// DOMParser drops invalid tags but inserts a <parsererror> element on failure
|
|
117
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/DOMParser/parseFromString#error_handling
|
|
118
|
+
if (doc.querySelector('parsererror') !== null) {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Do we have at least one real element node inside <body>?
|
|
123
|
+
return Array.from(doc.body.childNodes).some((n) => n.nodeType === Node.ELEMENT_NODE);
|
|
124
|
+
}
|
|
125
|
+
|
|
119
126
|
export function renderDuoChatMarkdownPreview(md, { trustedUrls = [] } = {}) {
|
|
120
127
|
if (!md) return '';
|
|
121
128
|
|
|
122
129
|
DOMPurify.addHook('beforeSanitizeElements', handleImageElements);
|
|
123
130
|
DOMPurify.addHook('afterSanitizeAttributes', sanitizeLinksHook(trustedUrls));
|
|
124
131
|
|
|
125
|
-
const parsedMarkdown = duoMarked.parse(md.toString());
|
|
132
|
+
const parsedMarkdown = isHtml(md) ? md : duoMarked.parse(md.toString());
|
|
133
|
+
|
|
126
134
|
const sanitized = DOMPurify.sanitize(parsedMarkdown, config);
|
|
127
135
|
|
|
128
136
|
DOMPurify.removeHook('beforeSanitizeElements');
|
|
129
137
|
DOMPurify.removeHook('afterSanitizeAttributes');
|
|
130
138
|
return sanitized;
|
|
131
139
|
}
|
|
140
|
+
|
|
141
|
+
export function addDuoMarkdownPlugin(plugin) {
|
|
142
|
+
duoMarked.use(plugin);
|
|
143
|
+
}
|
|
@@ -31,9 +31,40 @@ const MOCK_SOURCES = [
|
|
|
31
31
|
export const MOCK_RESPONSE_MESSAGE = {
|
|
32
32
|
id: '123',
|
|
33
33
|
content:
|
|
34
|
-
'
|
|
35
|
-
contentHtml:
|
|
36
|
-
|
|
34
|
+
'I\'ll write a simple Python function with comments for you. Here\'s an example:\\\\n\\\\n```python\\\\ndef calculate_factorial(n):\\\\n \\\\\\"\\\\\\"\\\\\\"\\\\n Calculate the factorial of a non-negative integer.\\\\n \\\\n Args:\\\\n n (int): A non-negative integer\\\\n \\\\n Returns:\\\\n int: The factorial of n (n!)\\\\n \\\\n Examples:\\\\n >>> calculate_factorial(5)\\\\n 120\\\\n >>> calculate_factorial(0)\\\\n 1\\\\n \\\\\\"\\\\\\"\\\\\\"\\\\n # Handle base cases\\\\n if n == 0 or n == 1:\\\\n return 1\\\\n \\\\n # Use recursion to calculate factorial\\\\n return n * calculate_factorial(n - 1)\\\\n```\\\\n\\\\nThis function calculates the factorial of a number using recursion. It includes a docstring explaining what the function does, its parameters, return value, and usage examples, plus inline comments explaining the logic.',
|
|
35
|
+
contentHtml: `<p dir="auto">I'd be happy to write a simple Python function with comments for you. </p>
|
|
36
|
+
<p dir="auto">Here's a basic Python function that calculates the factorial of a number:</p>
|
|
37
|
+
<div class="gl-relative markdown-code-block js-markdown-code"><pre><code class="language-python"><span class="hljs-keyword">def</span> <span class="hljs-title function_">factorial</span>(<span class="hljs-params">n</span>):
|
|
38
|
+
<span class="hljs-string">"""
|
|
39
|
+
Calculate the factorial of a non-negative integer.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
n (int): A non-negative integer
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
int: The factorial of n (n!)
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
>>> factorial(5)
|
|
49
|
+
120
|
|
50
|
+
>>> factorial(0)
|
|
51
|
+
1
|
|
52
|
+
"""</span>
|
|
53
|
+
<span class="hljs-comment"># Base case: factorial of 0 or 1 is 1</span>
|
|
54
|
+
<span class="hljs-keyword">if</span> n <= <span class="hljs-number">1</span>:
|
|
55
|
+
<span class="hljs-keyword">return</span> <span class="hljs-number">1</span>
|
|
56
|
+
|
|
57
|
+
<span class="hljs-comment"># Recursive case: n! = n * (n-1)!</span>
|
|
58
|
+
<span class="hljs-keyword">return</span> n * factorial(n - <span class="hljs-number">1</span>)
|
|
59
|
+
</code></pre>
|
|
60
|
+
<copy-code></copy-code><insert-code-snippet></insert-code-snippet></div><p dir="auto">This function includes:</p>
|
|
61
|
+
<ul dir="auto">
|
|
62
|
+
<li>A docstring explaining what the function does</li>
|
|
63
|
+
<li>Parameter and return type documentation</li>
|
|
64
|
+
<li>Usage examples</li>
|
|
65
|
+
<li>Inline comments explaining the logic</li>
|
|
66
|
+
</ul>
|
|
67
|
+
<p dir="auto">Is there a specific type of function you'd like me to create instead?</p>`,
|
|
37
68
|
role: MESSAGE_MODEL_ROLES.assistant,
|
|
38
69
|
extras: {
|
|
39
70
|
sources: MOCK_SOURCES,
|
package/src/index.js
CHANGED
|
@@ -26,3 +26,5 @@ export { default as DuoChatContextItemDetailsModal } from './components/chat/com
|
|
|
26
26
|
export { default as DuoChatContextItemMenu } from './components/chat/components/duo_chat_context/duo_chat_context_item_menu/duo_chat_context_item_menu.vue';
|
|
27
27
|
export { default as DuoChatContextItemPopover } from './components/chat/components/duo_chat_context/duo_chat_context_item_popover/duo_chat_context_item_popover.vue';
|
|
28
28
|
export { default as DuoChatContextItemSelections } from './components/chat/components/duo_chat_context/duo_chat_context_item_selections/duo_chat_context_item_selections.vue';
|
|
29
|
+
|
|
30
|
+
export { addDuoMarkdownPlugin } from './components/chat/markdown_renderer';
|