@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 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
- ALLOW_TAGS: ['p', '#text', 'div', 'code', 'insert-code-snippet', 'gl-markdown', 'pre', 'span', 'gl-compact-markdown', 'copy-code'],
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: 'Here is a simple JavaScript function to sum two numbers:\n\n ```js\n function sum(a, b) {\n return a + b;\n }\n ```\n \n To use it:\n \n ```js\n const result = sum(5, 3); // result = 8\n ```\n \n This function takes two number parameters, a and b. It returns the sum of adding them together.\n',
23
- contentHtml: '<p data-sourcepos="1:1-1:56" dir="auto">Here is a simple JavaScript function to sum two numbers:</p>\n<div class="gl-relative markdown-code-block js-markdown-code">\n<pre data-sourcepos="3:1-7:3" data-canonical-lang="js" class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="kd">function</span> <span class="nf">sum</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span></span>\n<span id="LC2" class="line" lang="javascript"> <span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span><span class="p">;</span></span>\n<span id="LC3" class="line" lang="javascript"><span class="p">}</span></span></code></pre>\n<copy-code></copy-code>\n<insert-code-snippet></insert-code-snippet>\n</div>\n<p data-sourcepos="9:1-9:10" dir="auto">To use it:</p>\n<div class="gl-relative markdown-code-block js-markdown-code">\n<pre data-sourcepos="11:1-13:3" data-canonical-lang="js" class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">sum</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span> <span class="c1">// result = 8</span></span></code></pre>\n<copy-code></copy-code>\n</div>\n<p data-sourcepos="15:1-15:95" dir="auto">This function takes two number parameters, a and b. It returns the sum of adding them together.</p>',
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&#39;d be happy to write a simple Python function with comments for you. </p>
24
+ <p dir="auto">Here&#39;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">&quot;&quot;&quot;
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
+ &gt;&gt;&gt; factorial(5)
37
+ 120
38
+ &gt;&gt;&gt; factorial(0)
39
+ 1
40
+ &quot;&quot;&quot;</span>
41
+ <span class="hljs-comment"># Base case: factorial of 0 or 1 is 1</span>
42
+ <span class="hljs-keyword">if</span> n &lt;= <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&#39;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.16.1",
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.131.0",
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
- ALLOW_TAGS: [
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
- 'Here is a simple JavaScript function to sum two numbers:\n\n ```js\n function sum(a, b) {\n return a + b;\n }\n ```\n \n To use it:\n \n ```js\n const result = sum(5, 3); // result = 8\n ```\n \n This function takes two number parameters, a and b. It returns the sum of adding them together.\n',
35
- contentHtml:
36
- '<p data-sourcepos="1:1-1:56" dir="auto">Here is a simple JavaScript function to sum two numbers:</p>\n<div class="gl-relative markdown-code-block js-markdown-code">\n<pre data-sourcepos="3:1-7:3" data-canonical-lang="js" class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="kd">function</span> <span class="nf">sum</span><span class="p">(</span><span class="nx">a</span><span class="p">,</span> <span class="nx">b</span><span class="p">)</span> <span class="p">{</span></span>\n<span id="LC2" class="line" lang="javascript"> <span class="k">return</span> <span class="nx">a</span> <span class="o">+</span> <span class="nx">b</span><span class="p">;</span></span>\n<span id="LC3" class="line" lang="javascript"><span class="p">}</span></span></code></pre>\n<copy-code></copy-code>\n<insert-code-snippet></insert-code-snippet>\n</div>\n<p data-sourcepos="9:1-9:10" dir="auto">To use it:</p>\n<div class="gl-relative markdown-code-block js-markdown-code">\n<pre data-sourcepos="11:1-13:3" data-canonical-lang="js" class="code highlight js-syntax-highlight language-javascript" lang="javascript" v-pre="true"><code><span id="LC1" class="line" lang="javascript"><span class="kd">const</span> <span class="nx">result</span> <span class="o">=</span> <span class="nf">sum</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span> <span class="c1">// result = 8</span></span></code></pre>\n<copy-code></copy-code>\n</div>\n<p data-sourcepos="15:1-15:95" dir="auto">This function takes two number parameters, a and b. It returns the sum of adding them together.</p>',
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&#39;d be happy to write a simple Python function with comments for you. </p>
36
+ <p dir="auto">Here&#39;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">&quot;&quot;&quot;
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
+ &gt;&gt;&gt; factorial(5)
49
+ 120
50
+ &gt;&gt;&gt; factorial(0)
51
+ 1
52
+ &quot;&quot;&quot;</span>
53
+ <span class="hljs-comment"># Base case: factorial of 0 or 1 is 1</span>
54
+ <span class="hljs-keyword">if</span> n &lt;= <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&#39;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';