Skip to content

Commit 42bcf9e

Browse files
committed
Updates to latest Graph component
- Adapts to new formatCommit callback
1 parent e834661 commit 42bcf9e

File tree

4 files changed

+110
-50
lines changed

4 files changed

+110
-50
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26099,7 +26099,7 @@
2609926099
"vscode:prepublish": "pnpm run bundle"
2610026100
},
2610126101
"dependencies": {
26102-
"@gitkraken/gitkraken-components": "13.0.0-vnext.10",
26102+
"@gitkraken/gitkraken-components": "13.0.0-vnext.15",
2610326103
"@gitkraken/provider-apis": "0.29.7",
2610426104
"@gitkraken/shared-web-components": "0.1.1-rc.15",
2610526105
"@gk-nzaytsev/fast-string-truncated-width": "1.1.0",

pnpm-lock.yaml

Lines changed: 8 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/webviews/apps/plus/graph/graph-wrapper/gl-graph.react.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import type { ReactElement } from 'react';
2121
import React, { createElement, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2222
import { getPlatform } from '@env/platform';
2323
import type { DateStyle } from '../../../../../config';
24+
import { splitCommitMessage } from '../../../../../git/utils/commit.utils';
2425
import type { DateTimeFormat } from '../../../../../system/date';
2526
import { formatDate, fromNow } from '../../../../../system/date';
2627
import { first, groupByFilterMap } from '../../../../../system/iterable';
@@ -566,14 +567,22 @@ export const GlGraphReact = memo((initProps: GraphWrapperInitProps) => {
566567
const emptyConfig = useMemo(() => ({}) as unknown as NonNullable<typeof props.config>, []);
567568
const config = useMemo(() => props.config ?? emptyConfig, [props.config, emptyConfig]);
568569

570+
const formatCommitMessage = (commitMessage: string) => {
571+
const { summary, body } = splitCommitMessage(commitMessage);
572+
573+
return {
574+
summary: <GlMarkdown markdown={summary} inline></GlMarkdown>,
575+
body: body ? <GlMarkdown markdown={body} inline></GlMarkdown> : undefined,
576+
};
577+
};
578+
569579
return (
570580
<GraphContainer
571581
ref={graphRef}
572582
avatarUrlByEmail={props.avatars}
573583
columnsSettings={props.columns}
574584
contexts={context}
575-
// @ts-expect-error returnType of formatCommitMessage callback expects to be string, but it works fine with react element
576-
formatCommitMessage={e => <GlMarkdown markdown={e}></GlMarkdown>}
585+
formatCommitMessage={formatCommitMessage}
577586
cssVariables={props.theming?.cssVariables}
578587
dimMergeCommits={config.dimMergeCommits}
579588
downstreamsByUpstream={props.downstreams}

src/webviews/apps/shared/components/markdown/markdown.ts

Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export class GlMarkdown extends LitElement {
1313
ruleStyles,
1414
css`
1515
:host {
16+
display: contents;
17+
1618
--markdown-compact-block-spacing: 8px;
1719
--markdown-list-spacing: 20px;
1820
}
@@ -105,34 +107,55 @@ export class GlMarkdown extends LitElement {
105107
li > ul {
106108
margin-top: 0;
107109
}
108-
`,
110+
= `,
109111
];
110112

111113
@property({ type: String })
112-
private markdown = '';
114+
markdown = '';
113115

114116
@property({ type: String, reflect: true })
115117
density: 'compact' | 'document' = 'compact';
116118

119+
@property({ type: Boolean, reflect: true })
120+
inline = false;
121+
117122
override render(): unknown {
118123
return html`${this.markdown ? until(this.renderMarkdown(this.markdown), 'Loading...') : ''}`;
119124
}
120125

121126
private async renderMarkdown(markdown: string) {
122-
marked.setOptions({
123-
gfm: true,
124-
// smartypants: true,
125-
// langPrefix: 'language-',
126-
});
127+
marked.setOptions({ gfm: true });
127128

128-
marked.use({ renderer: getMarkdownRenderer() });
129+
const renderer = this.inline ? getInlineMarkdownRenderer() : getMarkdownRenderer();
130+
marked.use({ renderer: renderer });
129131

130132
let rendered = await marked.parse(markdownEscapeEscapedIcons(markdown));
131133
rendered = renderThemeIconsWithinText(rendered);
132134
return unsafeHTML(rendered);
133135
}
134136
}
135137

138+
const escapeReplacements: { [index: string]: string } = {
139+
'&': '&amp;',
140+
'<': '&lt;',
141+
'>': '&gt;',
142+
'"': '&quot;',
143+
"'": '&#39;',
144+
};
145+
const getEscapeReplacement = (ch: string) => escapeReplacements[ch];
146+
147+
export function escape(html: string, encode?: boolean) {
148+
if (encode) {
149+
if (/[&<>"']/.test(html)) {
150+
return html.replace(/[&<>"']/g, getEscapeReplacement);
151+
}
152+
} else if (/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/.test(html)) {
153+
return html.replace(/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g, getEscapeReplacement);
154+
}
155+
156+
return html;
157+
}
158+
136159
function getMarkdownRenderer(): RendererObject {
137160
return {
138161
image: function (this: RendererThis, { href, title, text }: Tokens.Image): string {
@@ -157,37 +180,69 @@ function getMarkdownRenderer(): RendererObject {
157180
const text = this.parser.parseInline(tokens);
158181
return `<p>${text}</p>`;
159182
},
160-
link: function (this: RendererThis, { href, title, tokens }: Tokens.Link): string | false {
161-
if (typeof href !== 'string') return '';
162-
163-
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
164-
let text = this.parser.parseInline(tokens);
165-
if (href === text) {
166-
// raw link case
167-
text = removeMarkdownEscapes(text);
168-
}
169-
170-
title = typeof title === 'string' ? escapeDoubleQuotes(removeMarkdownEscapes(title)) : '';
183+
html: function (this: RendererThis, { text }: Tokens.HTML | Tokens.Tag): string {
184+
const match = text.match(/^(<span[^>]+>)|(<\/\s*span>)$/);
185+
return match ? text : '';
186+
},
187+
};
188+
}
171189

172-
// HTML Encode href
173-
href = removeMarkdownEscapes(href)
174-
.replace(/&/g, '&amp;')
175-
.replace(/</g, '&lt;')
176-
.replace(/>/g, '&gt;')
177-
.replace(/"/g, '&quot;')
178-
.replace(/'/g, '&#39;');
190+
function getInlineMarkdownRenderer(): RendererObject {
191+
let listIndex = 0;
192+
let isOrderedList = false;
193+
194+
const renderListItem = function (this: RendererThis, item: Tokens.ListItem): string {
195+
// In inline mode, render list item with symbol prefix
196+
const text = this.parser.parse(item.tokens, Boolean(item.loose));
197+
// Get the symbol: task checkbox, number for ordered, bullet for unordered
198+
let symbol: string;
199+
if (item.task) {
200+
symbol = item.checked ? '☑' : '☐';
201+
} else if (isOrderedList) {
202+
symbol = `${listIndex}.`;
203+
listIndex++;
204+
} else {
205+
symbol = '•';
206+
}
207+
return `${symbol} ${text.trim()} `;
208+
};
179209

180-
return `<a href="${href}" title="${title || href}" draggable="false">${text}</a>`;
210+
return {
211+
image: function (this: RendererThis, { text }: Tokens.Image): string {
212+
// In inline mode, use alt text if available, otherwise skip
213+
return text || '';
214+
},
215+
paragraph: function (this: RendererThis, { tokens }: Tokens.Paragraph): string {
216+
const text = this.parser.parseInline(tokens);
217+
return text;
181218
},
182-
code: function (this: RendererThis, { text, lang }: Tokens.Code): string {
183-
// Remote code may include characters that need to be escaped to be visible in HTML
184-
text = text.replace(/</g, '&lt;');
185-
return `<pre class="language-${lang}"><code>${text}</code></pre>`;
219+
list: function (this: RendererThis, token: Tokens.List): string {
220+
// In inline mode, render list items separated by spaces with their symbols
221+
isOrderedList = token.ordered;
222+
listIndex = typeof token.start === 'number' ? token.start : 1;
223+
let body = '';
224+
for (const item of token.items) {
225+
body += renderListItem.call(this, item);
226+
}
227+
return body;
228+
},
229+
listitem: renderListItem,
230+
link: function (this: RendererThis, { tokens }: Tokens.Link): string | false {
231+
const text = this.parser.parseInline(tokens);
232+
return text;
186233
},
187-
codespan: function (this: RendererThis, { text }: Tokens.Codespan): string {
188-
// Remote code may include characters that need to be escaped to be visible in HTML
189-
text = text.replace(/</g, '&lt;');
190-
return `<code>${text}</code>`;
234+
code: function (this: RendererThis, { text }: Tokens.Code): string {
235+
// In inline mode, wrap in code tag but without pre block formatting
236+
return `<code>${escape(text, true)}</code>`;
237+
},
238+
239+
br: function (): string {
240+
// In inline mode, render as a space instead of line break
241+
return ' ';
242+
},
243+
html: function (): string {
244+
// In inline mode, skip HTML tags
245+
return '';
191246
},
192247
};
193248
}
@@ -264,10 +319,3 @@ function renderThemeIcon(icon: ThemeIcon): string {
264319
function escapeDoubleQuotes(input: string) {
265320
return input.replace(/"/g, '&quot;');
266321
}
267-
268-
function removeMarkdownEscapes(text: string): string {
269-
if (!text) {
270-
return text;
271-
}
272-
return text.replace(/\\([\\`*_{}[\]()#+\-.!~])/g, '$1');
273-
}

0 commit comments

Comments
 (0)