Skip to content

Commit d4e281c

Browse files
paulirishDevtools-frontend LUCI CQ
authored andcommitted
DrJones/Perf: Strip links in markdown response
Bug:377601855 Change-Id: Id3298cdc285135b032956a4a56d708b144f5c015 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/5997916 Auto-Submit: Paul Irish <[email protected]> Reviewed-by: Adam Raine <[email protected]> Commit-Queue: Paul Irish <[email protected]> Commit-Queue: Adam Raine <[email protected]>
1 parent 4ad8e73 commit d4e281c

File tree

5 files changed

+99
-2
lines changed

5 files changed

+99
-2
lines changed

front_end/panels/freestyler/FreestylerPanel.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
219219
},
220220
selectedContext: null,
221221
blockedByCrossOrigin: false,
222+
stripLinks: false,
222223
isReadOnly: false,
223224
};
224225
}
@@ -520,7 +521,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
520521
return Common.Revealer.reveal(context.getItem().uiLocation(0, 0));
521522
}
522523
if (context instanceof CallTreeContext) {
523-
const trace = new SDK.TraceObject.RevealableEvent(event);
524+
const trace = new SDK.TraceObject.RevealableEvent(context.getItem().selectedNode.event);
524525
return Common.Revealer.reveal(trace);
525526
}
526527
// Node picker is using linkifier.
@@ -687,6 +688,7 @@ export class FreestylerPanel extends UI.Panel.Panel {
687688
this.#viewProps.isReadOnly = this.#currentAgent.isHistoryEntry;
688689
this.#viewProps.requiresNewConversation = this.#currentAgent.type === AgentType.DRJONES_PERFORMANCE &&
689690
Boolean(this.#currentAgent.context) && this.#currentAgent.context !== currentContext;
691+
this.#viewProps.stripLinks = this.#viewProps.agentType === AgentType.DRJONES_PERFORMANCE;
690692
this.doUpdate();
691693
}
692694

front_end/panels/freestyler/components/FreestylerChatUi.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,70 @@ css
3535
}`,
3636
}));
3737
});
38+
39+
describe('link/image stripping', () => {
40+
const linkCases = [
41+
'[link text](https://z.com)',
42+
'A response with [link text](https://z.com).',
43+
'[*link text*](https://z.com)',
44+
'[**text** `with code`](https://z.com).',
45+
'plain link https://z.com .',
46+
'link in quotes \'https://z.com\' .',
47+
];
48+
49+
const renderToElem = (string: string, renderer: MarkdownView.MarkdownView.MarkdownLitRenderer): Element => {
50+
const component = new MarkdownView.MarkdownView.MarkdownView();
51+
renderElementIntoDOM(component, {allowMultipleChildren: true});
52+
component.data = {tokens: Marked.Marked.lexer(string), renderer};
53+
assert.exists(component.shadowRoot?.firstElementChild);
54+
return component.shadowRoot.firstElementChild;
55+
};
56+
57+
it('strips links if stripLinks true', () => {
58+
const linklessRenderer = new MarkdownRendererWithCodeBlock({stripLinks: true});
59+
for (const linkCase of linkCases) {
60+
const elem = renderToElem(linkCase, linklessRenderer);
61+
assert.strictEqual(elem.querySelectorAll('a, x-link, devtools-link').length, 0);
62+
assert.strictEqual(
63+
['<a', '<x-link', '<devtools-link'].some(tagName => elem.outerHTML.includes(tagName)), false);
64+
assert.ok(elem.textContent?.includes('( https://z.com )'), linkCase);
65+
}
66+
});
67+
68+
it('leaves links intact by default', () => {
69+
const linkfulRenderer = new MarkdownRendererWithCodeBlock();
70+
for (const linkCase of linkCases) {
71+
const elem = renderToElem(linkCase, linkfulRenderer);
72+
assert.strictEqual(elem.querySelectorAll('a, x-link, devtools-link').length, 1);
73+
assert.strictEqual(
74+
['<a', '<x-link', '<devtools-link'].some(tagName => elem.outerHTML.includes(tagName)), true);
75+
assert.strictEqual(elem.textContent?.includes('( https://z.com )'), false);
76+
}
77+
});
78+
79+
const imageCases = [
80+
'![image alt](https://z.com/i.png)',
81+
'A response with ![image alt](https://z.com/i.png).',
82+
'![*image alt*](https://z.com/i.png)',
83+
'![**text** `with code`](https://z.com/i.png).',
84+
'plain image href https://z.com/i.png .',
85+
'link in quotes \'https://z.com/i.png\' .',
86+
];
87+
88+
it('strips images if stripLinks true', () => {
89+
const linklessRenderer = new MarkdownRendererWithCodeBlock({stripLinks: true});
90+
for (const imageCase of imageCases) {
91+
const elem = renderToElem(imageCase, linklessRenderer);
92+
assert.strictEqual(elem.querySelectorAll('a, x-link, devtools-link, img, devtools-markdown-image').length, 0);
93+
assert.strictEqual(
94+
['<a', '<x-link', '<devtools-link', '<img', '<devtools-markdown-image'].some(
95+
tagName => elem.outerHTML.includes(tagName)),
96+
false);
97+
98+
assert.ok(elem.textContent?.includes('( https://z.com/i.png )'), imageCase);
99+
}
100+
});
101+
});
38102
});
39103

40104
function getProp(options: Partial<Freestyler.Props>): Freestyler.Props {
@@ -57,6 +121,7 @@ css
57121
canShowFeedbackForm: false,
58122
userInfo: {},
59123
blockedByCrossOrigin: false,
124+
stripLinks: false,
60125
isReadOnly: false,
61126
...options,
62127
};

front_end/panels/freestyler/components/FreestylerChatUi.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export interface Props {
281281
isReadOnly: boolean;
282282
blockedByCrossOrigin: boolean;
283283
requiresNewConversation?: boolean;
284+
stripLinks: boolean;
284285
}
285286

286287
// The model returns multiline code blocks in an erroneous way with the language being in new line.
@@ -294,6 +295,11 @@ export interface Props {
294295
// }
295296
// ```
296297
class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.MarkdownInsightRenderer {
298+
#stripLinks: boolean = false;
299+
constructor(opts: {stripLinks?: boolean} = {}) {
300+
super();
301+
this.#stripLinks = Boolean(opts.stripLinks);
302+
}
297303
override templateForToken(token: Marked.Marked.MarkedToken): LitHtml.TemplateResult|null {
298304
if (token.type === 'code') {
299305
const lines = (token.text as string).split('\n');
@@ -303,13 +309,34 @@ class MarkdownRendererWithCodeBlock extends MarkdownView.MarkdownView.MarkdownIn
303309
}
304310
}
305311

312+
// Potentially remove links from the rendered result
313+
if (this.#stripLinks && (token.type === 'link' || token.type === 'image')) {
314+
// Insert an extra text node at the end after any link text. Show the link as plaintext (surrounded by parentheses)
315+
const urlText = ` ( ${token.href} )`;
316+
// Images would be turned into as links (but we'll skip that) https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/ui/components/markdown_view/MarkdownView.ts;l=286-291;drc=d2cc89e48c913666655542d818ad0a09d25d0d08
317+
const childTokens = token.type === 'image' ? undefined : [
318+
...token.tokens,
319+
{type: 'text', text: urlText, raw: urlText},
320+
];
321+
322+
token = {
323+
...token,
324+
// Marked doesn't read .text or .raw of a link, but we'll update anyway
325+
// https://github.com/markedjs/marked/blob/035af38ab1e5aae95ece213dcc9a9c6d79cff46f/src/Renderer.ts#L178-L191
326+
text: `${token.text}${urlText}`,
327+
raw: `${token.text}${urlText}`,
328+
type: 'text',
329+
tokens: childTokens,
330+
};
331+
}
332+
306333
return super.templateForToken(token);
307334
}
308335
}
309336

310337
export class FreestylerChatUi extends HTMLElement {
311338
readonly #shadow = this.attachShadow({mode: 'open'});
312-
readonly #markdownRenderer = new MarkdownRendererWithCodeBlock();
339+
#markdownRenderer = new MarkdownRendererWithCodeBlock();
313340
#scrollTop?: number;
314341
#props: Props;
315342

@@ -319,6 +346,7 @@ export class FreestylerChatUi extends HTMLElement {
319346
}
320347

321348
set props(props: Props) {
349+
this.#markdownRenderer = new MarkdownRendererWithCodeBlock({stripLinks: props.stripLinks});
322350
this.#props = props;
323351
this.#render();
324352
}

front_end/ui/components/docs/freestyler/basic.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const component = new Freestyler.FreestylerChatUi({
5656
userInfo: {},
5757
blockedByCrossOrigin: false,
5858
isReadOnly: false,
59+
stripLinks: false,
5960
});
6061

6162
document.getElementById('container')?.appendChild(component);

front_end/ui/components/docs/freestyler/empty_state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const component = new Freestyler.FreestylerChatUi({
3131
userInfo: {},
3232
blockedByCrossOrigin: false,
3333
isReadOnly: false,
34+
stripLinks: false,
3435
});
3536

3637
document.getElementById('container')?.appendChild(component);

0 commit comments

Comments
 (0)