Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { $ } from '../../../../../base/browser/dom.js';
import { $, reset } from '../../../../../base/browser/dom.js';
import { IRenderedMarkdown, MarkdownRenderOptions } from '../../../../../base/browser/markdownRenderer.js';
import { getDefaultHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
Expand All @@ -19,6 +19,25 @@ import { AGENT_HOST_SCHEME } from '../../../../../platform/agentHost/common/agen

const _remoteImageDisallowed = () => false;

const nonPlainTextMarkdownSyntax = /[\\`*_[\]<>|&$~]/;
const gfmAutolink = /\b(?:https?:\/\/|www\.)/i;
const blockMarkdownSyntax = /(^|\n)\s{0,3}(?:#{1,6}\s|>\s?|[-+]\s|\d+[.)]\s|---+\s*$)/;

function renderPlainTextMarkdown(markdown: IMarkdownString, outElement?: HTMLElement): IRenderedMarkdown | undefined {
const value = markdown.value;
if (!value || nonPlainTextMarkdownSyntax.test(value) || gfmAutolink.test(value) || blockMarkdownSyntax.test(value)) {
return undefined;
}

const element = outElement ?? $('div');
element.classList.add('rendered-markdown');
reset(element, $('p', undefined, value.length > 100_000 ? `${value.substr(0, 100_000)}…` : value));
return {
element,
dispose: () => { }
};
}

export const allowedChatMarkdownHtmlTags = Object.freeze([
'b',
'blockquote',
Expand Down Expand Up @@ -89,6 +108,11 @@ export class ChatContentMarkdownRenderer implements IMarkdownRenderer {
) { }

render(markdown: IMarkdownString, options?: MarkdownRenderOptions, outElement?: HTMLElement): IRenderedMarkdown {
const plainTextResult = renderPlainTextMarkdown(markdown, outElement);
if (plainTextResult) {
return plainTextResult;
}

options = getChatMarkdownRenderOptions(options);

const mdWithBody: IMarkdownString = (markdown && markdown.supportHtml) ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,34 @@ suite('ChatMarkdownRenderer', () => {
await assertSnapshot(result.element.textContent);
});

test('plain text fast path preserves rendered markdown shape', () => {
const md = new MarkdownString('Hello, world. This is plain.', { isTrusted: true, supportHtml: true, supportThemeIcons: true });
const result = store.add(testRenderer.render(md));

assert.deepStrictEqual({
outerHTML: result.element.outerHTML,
textContent: result.element.textContent,
}, {
outerHTML: '<div class="rendered-markdown"><p>Hello, world. This is plain.</p></div>',
textContent: 'Hello, world. This is plain.',
});
});

test('plain text fast path reuses target element', () => {
const md = new MarkdownString('Hello, world.');
const target = document.createElement('div');
target.appendChild(document.createElement('span'));
const result = store.add(testRenderer.render(md, undefined, target));

assert.deepStrictEqual({
sameElement: result.element === target,
outerHTML: target.outerHTML,
}, {
sameElement: true,
outerHTML: '<div class="rendered-markdown"><p>Hello, world.</p></div>',
});
});

test('supportHtml with one-line markdown', async () => {
const md = new MarkdownString('**hello**');
md.supportHtml = true;
Expand Down
Loading