From 199e63416a7e6dee51c7ee71eb2356ecb17e3552 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Mon, 23 Sep 2024 12:52:33 +0200 Subject: [PATCH 1/6] fix markdown request renderer Change the rendering off all markdown renderers to use markdown-it directly. Also no MarkdownString class is used anymore. fixes #14208 --- .../markdown-part-renderer.tsx | 45 +++++++++++++------ .../chat-tree-view/chat-view-tree-widget.tsx | 41 ++++++++++------- packages/ai-chat/src/common/chat-model.ts | 27 ++++++----- 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx index a2fcc7329d53b..0fa694e290b36 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -15,7 +15,7 @@ // ***************************************************************************** import { ChatResponsePartRenderer } from '../chat-response-part-renderer'; -import { inject, injectable } from '@theia/core/shared/inversify'; +import { injectable } from '@theia/core/shared/inversify'; import { ChatResponseContent, InformationalChatResponseContent, @@ -23,12 +23,12 @@ import { } from '@theia/ai-chat/lib/common'; import { ReactNode, useEffect, useRef } from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react'; -import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; -import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; +import * as markdownit from '@theia/core/shared/markdown-it'; +import * as DOMPurify from '@theia/core/shared/dompurify'; @injectable() export class MarkdownPartRenderer implements ChatResponsePartRenderer { - @inject(MarkdownRenderer) private renderer: MarkdownRenderer; + protected readonly markdownIt = markdownit(); canHandle(response: ChatResponseContent): number { if (MarkdownChatResponseContent.is(response)) { return 10; @@ -38,34 +38,51 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer = useRef(null); + + // useEffect(() => { + // const host = document.createElement('div'); + // const html = this.markdownIt.render(response.content); + // host.innerHTML = DOMPurify.sanitize(html, { + // ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs + // }); + // while (ref?.current?.firstChild) { + // ref.current.removeChild(ref.current.firstChild); + // } + + // ref?.current?.appendChild(host); + // }, [response.content]); // TODO let the user configure whether they want to see informational content if (InformationalChatResponseContent.is(response)) { // null is valid in React // eslint-disable-next-line no-null/no-null return null; } - return ; + + // return
; + return ; } } -export const MarkdownWrapper = (props: { data: MarkdownString, renderCallback: (md: MarkdownString) => HTMLElement }) => { +const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | InformationalChatResponseContent }) => { // eslint-disable-next-line no-null/no-null const ref: React.MutableRefObject = useRef(null); - useEffect(() => { - const myDomElement = props.renderCallback(props.data); - + const markdownIt = markdownit(); + const host = document.createElement('div'); + const html = markdownIt.render(response.content); + host.innerHTML = DOMPurify.sanitize(html, { + ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs + }); while (ref?.current?.firstChild) { ref.current.removeChild(ref.current.firstChild); } - ref?.current?.appendChild(myDomElement); - }, [props.data.value]); + ref?.current?.appendChild(host); + }, [response.content]); return
; }; diff --git a/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx index 04fa4d4253c5c..9f026978597bf 100644 --- a/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx @@ -35,7 +35,6 @@ import { TreeProps, TreeWidget, } from '@theia/core/lib/browser'; -import { MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering/markdown-string'; import { inject, injectable, @@ -44,10 +43,11 @@ import { } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; -import { MarkdownRenderer } from '@theia/core/lib/browser/markdown-rendering/markdown-renderer'; import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution'; import { ChatResponsePartRenderer } from '../chat-response-part-renderer'; -import { MarkdownWrapper } from '../chat-response-renderer/markdown-part-renderer'; +import * as markdownit from '@theia/core/shared/markdown-it'; +import * as DOMPurify from '@theia/core/shared/dompurify'; +import { useEffect, useRef } from '@theia/core/shared/react'; // TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model export interface RequestNode extends TreeNode { @@ -76,9 +76,6 @@ export class ChatViewTreeWidget extends TreeWidget { @inject(ContributionProvider) @named(ChatNodeToolbarActionContribution) protected readonly chatNodeToolbarActionContributions: ContributionProvider; - @inject(MarkdownRenderer) - private renderer: MarkdownRenderer; - @inject(ChatAgentService) protected chatAgentService: ChatAgentService; @@ -336,16 +333,7 @@ export class ChatViewTreeWidget extends TreeWidget { } private renderChatRequest(node: RequestNode): React.ReactNode { - const text = node.request.request.displayText ?? node.request.request.text; - const markdownString = new MarkdownStringImpl(text, { supportHtml: true, isTrusted: true }); - return ( -
- { this.renderer.render(markdownString).element} - >} -
- ); + return ; } private renderChatResponse(node: ResponseNode): React.ReactNode { @@ -389,6 +377,27 @@ export class ChatViewTreeWidget extends TreeWidget { } } +const ChatRequestRender = ({ node }: { node: RequestNode }) => { + // eslint-disable-next-line no-null/no-null + const ref: React.MutableRefObject = useRef(null); + useEffect(() => { + const markdownIt = markdownit(); + const text = node.request.request.displayText ?? node.request.request.text; + const host = document.createElement('div'); + const html = markdownIt.render(text); + host.innerHTML = DOMPurify.sanitize(html, { + ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs + }); + while (ref?.current?.firstChild) { + ref.current.removeChild(ref.current.firstChild); + } + + ref?.current?.appendChild(host); + }, [node.request]); + + return
; +}; + const ProgressMessage = (c: ChatProgressMessage) => (
{c.content} diff --git a/packages/ai-chat/src/common/chat-model.ts b/packages/ai-chat/src/common/chat-model.ts index 777cb0bac21e7..7e1c3c5d6e766 100644 --- a/packages/ai-chat/src/common/chat-model.ts +++ b/packages/ai-chat/src/common/chat-model.ts @@ -20,7 +20,6 @@ // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts import { Command, Emitter, Event, generateUuid, URI } from '@theia/core'; -import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering'; import { Position } from '@theia/core/shared/vscode-languageserver-protocol'; import { ChatAgentLocation } from './chat-agents'; import { ParsedChatRequest } from './parsed-chat-request'; @@ -128,7 +127,7 @@ export interface ErrorChatResponseContent extends ChatResponseContent { export interface MarkdownChatResponseContent extends Required { kind: 'markdownContent'; - content: MarkdownString; + content: string; } export interface CodeChatResponseContent @@ -187,7 +186,7 @@ export interface CommandChatResponseContent extends ChatResponseContent { */ export interface InformationalChatResponseContent extends ChatResponseContent { kind: 'informational'; - content: MarkdownString; + content: string; } export namespace TextChatResponseContent { @@ -207,7 +206,7 @@ export namespace MarkdownChatResponseContent { ChatResponseContent.is(obj) && obj.kind === 'markdownContent' && 'content' in obj && - MarkdownString.is((obj as { content: unknown }).content) + typeof (obj as { content: unknown }).content === 'string' ); } } @@ -218,7 +217,7 @@ export namespace InformationalChatResponseContent { ChatResponseContent.is(obj) && obj.kind === 'informational' && 'content' in obj && - MarkdownString.is((obj as { content: unknown }).content) + typeof (obj as { content: unknown }).content === 'string' ); } } @@ -411,35 +410,35 @@ export class TextChatResponseContentImpl implements TextChatResponseContent { export class MarkdownChatResponseContentImpl implements MarkdownChatResponseContent { readonly kind = 'markdownContent'; - protected _content: MarkdownStringImpl = new MarkdownStringImpl(); + protected _content: string; constructor(content: string) { - this._content.appendMarkdown(content); + this._content = content; } - get content(): MarkdownString { + get content(): string { return this._content; } asString(): string { - return this._content.value; + return this._content; } merge(nextChatResponseContent: MarkdownChatResponseContent): boolean { - this._content.appendMarkdown(nextChatResponseContent.content.value); + this._content += nextChatResponseContent.content; return true; } } export class InformationalChatResponseContentImpl implements InformationalChatResponseContent { readonly kind = 'informational'; - protected _content: MarkdownStringImpl; + protected _content: string; constructor(content: string) { - this._content = new MarkdownStringImpl(content); + this._content = content; } - get content(): MarkdownString { + get content(): string { return this._content; } @@ -448,7 +447,7 @@ export class InformationalChatResponseContentImpl implements InformationalChatRe } merge(nextChatResponseContent: InformationalChatResponseContent): boolean { - this._content.appendMarkdown(nextChatResponseContent.content.value); + this._content += nextChatResponseContent.content; return true; } } From 0a6f8151523bf5f2aefe5464b8441b9a2ad2cf55 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Thu, 26 Sep 2024 00:29:46 +0200 Subject: [PATCH 2/6] incorporate review comments - revert model changes - create custom hook for rendering --- .../markdown-part-renderer.tsx | 40 ++++++++++--------- .../chat-tree-view/chat-view-tree-widget.tsx | 22 ++-------- packages/ai-chat/src/common/chat-model.ts | 27 +++++++------ 3 files changed, 38 insertions(+), 51 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx index 0fa694e290b36..9ea2f78adec94 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -39,21 +39,6 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer = useRef(null); - - // useEffect(() => { - // const host = document.createElement('div'); - // const html = this.markdownIt.render(response.content); - // host.innerHTML = DOMPurify.sanitize(html, { - // ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs - // }); - // while (ref?.current?.firstChild) { - // ref.current.removeChild(ref.current.firstChild); - // } - - // ref?.current?.appendChild(host); - // }, [response.content]); // TODO let the user configure whether they want to see informational content if (InformationalChatResponseContent.is(response)) { // null is valid in React @@ -69,11 +54,28 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer { // eslint-disable-next-line no-null/no-null - const ref: React.MutableRefObject = useRef(null); + const ref = useMarkdownRendering(response.content.value); + + return
; +}; + +/** + * This hook uses markdown-it directly to render markdown. + * The reason to use markdown-it directly is that the MarkdownRenderer is + * overriden by theia with a monaco version. This monaco version strips all html + * tags from the markdown with empty content. + * This leads to unexpected behavior when rendering markdown with html tags. + * + * @param markdown the string to render as markdown + * @returns the ref to use in an element to render the markdown + */ +export const useMarkdownRendering = (markdown: string) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); useEffect(() => { const markdownIt = markdownit(); const host = document.createElement('div'); - const html = markdownIt.render(response.content); + const html = markdownIt.render(markdown); host.innerHTML = DOMPurify.sanitize(html, { ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs }); @@ -82,7 +84,7 @@ const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | } ref?.current?.appendChild(host); - }, [response.content]); + }, [markdown]); - return
; + return ref; }; diff --git a/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx index 9f026978597bf..19d7daacdd9b8 100644 --- a/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx +++ b/packages/ai-chat-ui/src/browser/chat-tree-view/chat-view-tree-widget.tsx @@ -45,9 +45,7 @@ import * as React from '@theia/core/shared/react'; import { ChatNodeToolbarActionContribution } from '../chat-node-toolbar-action-contribution'; import { ChatResponsePartRenderer } from '../chat-response-part-renderer'; -import * as markdownit from '@theia/core/shared/markdown-it'; -import * as DOMPurify from '@theia/core/shared/dompurify'; -import { useEffect, useRef } from '@theia/core/shared/react'; +import { useMarkdownRendering } from '../chat-response-renderer/markdown-part-renderer'; // TODO Instead of directly operating on the ChatRequestModel we could use an intermediate view model export interface RequestNode extends TreeNode { @@ -378,22 +376,8 @@ export class ChatViewTreeWidget extends TreeWidget { } const ChatRequestRender = ({ node }: { node: RequestNode }) => { - // eslint-disable-next-line no-null/no-null - const ref: React.MutableRefObject = useRef(null); - useEffect(() => { - const markdownIt = markdownit(); - const text = node.request.request.displayText ?? node.request.request.text; - const host = document.createElement('div'); - const html = markdownIt.render(text); - host.innerHTML = DOMPurify.sanitize(html, { - ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs - }); - while (ref?.current?.firstChild) { - ref.current.removeChild(ref.current.firstChild); - } - - ref?.current?.appendChild(host); - }, [node.request]); + const text = node.request.request.displayText ?? node.request.request.text; + const ref = useMarkdownRendering(text); return
; }; diff --git a/packages/ai-chat/src/common/chat-model.ts b/packages/ai-chat/src/common/chat-model.ts index 7e1c3c5d6e766..777cb0bac21e7 100644 --- a/packages/ai-chat/src/common/chat-model.ts +++ b/packages/ai-chat/src/common/chat-model.ts @@ -20,6 +20,7 @@ // Partially copied from https://github.com/microsoft/vscode/blob/a2cab7255c0df424027be05d58e1b7b941f4ea60/src/vs/workbench/contrib/chat/common/chatModel.ts import { Command, Emitter, Event, generateUuid, URI } from '@theia/core'; +import { MarkdownString, MarkdownStringImpl } from '@theia/core/lib/common/markdown-rendering'; import { Position } from '@theia/core/shared/vscode-languageserver-protocol'; import { ChatAgentLocation } from './chat-agents'; import { ParsedChatRequest } from './parsed-chat-request'; @@ -127,7 +128,7 @@ export interface ErrorChatResponseContent extends ChatResponseContent { export interface MarkdownChatResponseContent extends Required { kind: 'markdownContent'; - content: string; + content: MarkdownString; } export interface CodeChatResponseContent @@ -186,7 +187,7 @@ export interface CommandChatResponseContent extends ChatResponseContent { */ export interface InformationalChatResponseContent extends ChatResponseContent { kind: 'informational'; - content: string; + content: MarkdownString; } export namespace TextChatResponseContent { @@ -206,7 +207,7 @@ export namespace MarkdownChatResponseContent { ChatResponseContent.is(obj) && obj.kind === 'markdownContent' && 'content' in obj && - typeof (obj as { content: unknown }).content === 'string' + MarkdownString.is((obj as { content: unknown }).content) ); } } @@ -217,7 +218,7 @@ export namespace InformationalChatResponseContent { ChatResponseContent.is(obj) && obj.kind === 'informational' && 'content' in obj && - typeof (obj as { content: unknown }).content === 'string' + MarkdownString.is((obj as { content: unknown }).content) ); } } @@ -410,35 +411,35 @@ export class TextChatResponseContentImpl implements TextChatResponseContent { export class MarkdownChatResponseContentImpl implements MarkdownChatResponseContent { readonly kind = 'markdownContent'; - protected _content: string; + protected _content: MarkdownStringImpl = new MarkdownStringImpl(); constructor(content: string) { - this._content = content; + this._content.appendMarkdown(content); } - get content(): string { + get content(): MarkdownString { return this._content; } asString(): string { - return this._content; + return this._content.value; } merge(nextChatResponseContent: MarkdownChatResponseContent): boolean { - this._content += nextChatResponseContent.content; + this._content.appendMarkdown(nextChatResponseContent.content.value); return true; } } export class InformationalChatResponseContentImpl implements InformationalChatResponseContent { readonly kind = 'informational'; - protected _content: string; + protected _content: MarkdownStringImpl; constructor(content: string) { - this._content = content; + this._content = new MarkdownStringImpl(content); } - get content(): string { + get content(): MarkdownString { return this._content; } @@ -447,7 +448,7 @@ export class InformationalChatResponseContentImpl implements InformationalChatRe } merge(nextChatResponseContent: InformationalChatResponseContent): boolean { - this._content += nextChatResponseContent.content; + this._content.appendMarkdown(nextChatResponseContent.content.value); return true; } } From 9b083f513421922d923509c2b65f694e97dc1178 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Thu, 26 Sep 2024 09:49:05 +0200 Subject: [PATCH 3/6] Update packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx Co-authored-by: Stefan Dirix --- .../browser/chat-response-renderer/markdown-part-renderer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx index 9ea2f78adec94..f8be393b0d85a 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -53,7 +53,6 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer { - // eslint-disable-next-line no-null/no-null const ref = useMarkdownRendering(response.content.value); return
; From f7915007da4de1737c1e8b1dc0c8fb1b0a82f108 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Thu, 26 Sep 2024 09:51:33 +0200 Subject: [PATCH 4/6] Update packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx Co-authored-by: Stefan Dirix --- .../browser/chat-response-renderer/markdown-part-renderer.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx index f8be393b0d85a..0f41dc2f7da30 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -46,7 +46,6 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer
; return ; } From 88424720dd681d4db201b4cd0f1de483036081d8 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Thu, 26 Sep 2024 10:18:19 +0200 Subject: [PATCH 5/6] allow to use MarkdownString or string in custom hook --- .../chat-response-renderer/markdown-part-renderer.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx index 0f41dc2f7da30..e098d142d9ae0 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -25,6 +25,7 @@ import { ReactNode, useEffect, useRef } from '@theia/core/shared/react'; import * as React from '@theia/core/shared/react'; import * as markdownit from '@theia/core/shared/markdown-it'; import * as DOMPurify from '@theia/core/shared/dompurify'; +import { MarkdownString } from '@theia/core/lib/common/markdown-rendering'; @injectable() export class MarkdownPartRenderer implements ChatResponsePartRenderer { @@ -52,7 +53,7 @@ export class MarkdownPartRenderer implements ChatResponsePartRenderer { - const ref = useMarkdownRendering(response.content.value); + const ref = useMarkdownRendering(response.content); return
; }; @@ -67,13 +68,14 @@ const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | * @param markdown the string to render as markdown * @returns the ref to use in an element to render the markdown */ -export const useMarkdownRendering = (markdown: string) => { +export const useMarkdownRendering = (markdown: string | MarkdownString) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); useEffect(() => { const markdownIt = markdownit(); const host = document.createElement('div'); - const html = markdownIt.render(markdown); + const markdownString = typeof markdown === 'string' ? markdown : markdown.value; + const html = markdownIt.render(markdownString); host.innerHTML = DOMPurify.sanitize(html, { ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs }); From 7163a858582d33922a0fa9d9241d7c6fc645d517 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Thu, 26 Sep 2024 11:03:14 +0200 Subject: [PATCH 6/6] fix rendering --- .../browser/chat-response-renderer/markdown-part-renderer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx index e098d142d9ae0..8fba51ad2fcbd 100644 --- a/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx +++ b/packages/ai-chat-ui/src/browser/chat-response-renderer/markdown-part-renderer.tsx @@ -71,10 +71,10 @@ const MarkdownRender = ({ response }: { response: MarkdownChatResponseContent | export const useMarkdownRendering = (markdown: string | MarkdownString) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); + const markdownString = typeof markdown === 'string' ? markdown : markdown.value; useEffect(() => { const markdownIt = markdownit(); const host = document.createElement('div'); - const markdownString = typeof markdown === 'string' ? markdown : markdown.value; const html = markdownIt.render(markdownString); host.innerHTML = DOMPurify.sanitize(html, { ALLOW_UNKNOWN_PROTOCOLS: true // DOMPurify usually strips non http(s) links from hrefs @@ -84,7 +84,7 @@ export const useMarkdownRendering = (markdown: string | MarkdownString) => { } ref?.current?.appendChild(host); - }, [markdown]); + }, [markdownString]); return ref; };