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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
### Added

- Adds [Cursor](https://cursor.so) support — closes [#3222](https://github.com/gitkraken/vscode-gitlens/issues/3222)
- Adds monospace formatting in commit messages — closes [#2350](https://github.com/gitkraken/vscode-gitlens/issues/2350)

### Changed

Expand Down
3 changes: 2 additions & 1 deletion src/annotations/autolinks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ import { debug } from '../system/decorators/log';
import { encodeUrl } from '../system/encoding';
import { join, map } from '../system/iterable';
import { Logger } from '../system/logger';
import { escapeMarkdown } from '../system/markdown';
import type { MaybePausedResult } from '../system/promise';
import { capitalize, encodeHtmlWeak, escapeMarkdown, escapeRegex, getSuperscript } from '../system/string';
import { capitalize, encodeHtmlWeak, escapeRegex, getSuperscript } from '../system/string';
import { configuration } from '../system/vscode/configuration';

const emptyAutolinkMap = Object.freeze(new Map<string, Autolink>());
Expand Down
5 changes: 3 additions & 2 deletions src/git/formatters/commitFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ import { emojify } from '../../emojis';
import { arePlusFeaturesEnabled } from '../../plus/gk/utils';
import type { ShowInCommitGraphCommandArgs } from '../../plus/webviews/graph/protocol';
import { join, map } from '../../system/iterable';
import { escapeMarkdown } from '../../system/markdown';
import { isPromise } from '../../system/promise';
import type { TokenOptions } from '../../system/string';
import { encodeHtmlWeak, escapeMarkdown, getSuperscript } from '../../system/string';
import { encodeHtmlWeak, getSuperscript } from '../../system/string';
import { configuration } from '../../system/vscode/configuration';
import type { ContactPresence } from '../../vsls/vsls';
import type { PreviousLineComparisonUrisResult } from '../gitProvider';
Expand Down Expand Up @@ -692,7 +693,7 @@ export class CommitFormatter extends Formatter<GitCommit, CommitFormatOptions> {
message = encodeHtmlWeak(message);
}
if (outputFormat === 'markdown') {
message = escapeMarkdown(message, { quoted: true });
message = escapeMarkdown(message, { quoted: true, inlineBackticks: true });
}

if (this._options.messageAutolinks) {
Expand Down
3 changes: 2 additions & 1 deletion src/git/remotes/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import type { Brand, Unbrand } from '../../system/brand';
import { fromNow } from '../../system/date';
import { memoize } from '../../system/decorators/memoize';
import { encodeUrl } from '../../system/encoding';
import { equalsIgnoreCase, escapeMarkdown, unescapeMarkdown } from '../../system/string';
import { escapeMarkdown, unescapeMarkdown } from '../../system/markdown';
import { equalsIgnoreCase } from '../../system/string';
import { getIssueOrPullRequestMarkdownIcon } from '../models/issue';
import { isSha } from '../models/reference';
import type { Repository } from '../models/repository';
Expand Down
3 changes: 2 additions & 1 deletion src/git/remotes/gitlab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import type { Brand, Unbrand } from '../../system/brand';
import { fromNow } from '../../system/date';
import { memoize } from '../../system/decorators/memoize';
import { encodeUrl } from '../../system/encoding';
import { equalsIgnoreCase, escapeMarkdown, unescapeMarkdown } from '../../system/string';
import { escapeMarkdown, unescapeMarkdown } from '../../system/markdown';
import { equalsIgnoreCase } from '../../system/string';
import { getIssueOrPullRequestMarkdownIcon } from '../models/issue';
import { isSha } from '../models/reference';
import type { Repository } from '../models/repository';
Expand Down
87 changes: 87 additions & 0 deletions src/system/markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
const escapeMarkdownRegex = /[\\*_{}[\]()#+\-.!]/g;
const unescapeMarkdownRegex = /\\([\\`*_{}[\]()#+\-.!])/g;

const escapeMarkdownHeaderRegex = /^===/gm;
const unescapeMarkdownHeaderRegex = /^\u200b===/gm;

// const sampleMarkdown = '## message `not code` *not important* _no underline_ \n> don\'t quote me \n- don\'t list me \n+ don\'t list me \n1. don\'t list me \nnot h1 \n=== \nnot h2 \n---\n***\n---\n___';
const markdownQuotedRegex = /\r?\n/g;
const markdownBacktickRegex = /`/g;

export function escapeMarkdown(s: string, options: { quoted?: boolean; inlineBackticks?: boolean } = {}): string {
s = s
// Escape markdown
.replace(escapeMarkdownRegex, '\\$&')
// Escape markdown header (since the above regex won't match it)
.replace(escapeMarkdownHeaderRegex, '\u200b===');

if (options.inlineBackticks) {
s = escapeMarkdownCodeBlocks(s);
} else {
s = s.replace(markdownBacktickRegex, '\\$&');
}
if (!options.quoted) return s;

// Keep under the same block-quote but with line breaks
return s.trim().replace(markdownQuotedRegex, '\t\\\n> ');
}

/**
* escapes markdown code blocks
*/
export function escapeMarkdownCodeBlocks(s: string) {
const tripleBackticks = '```';
const escapedTripleBackticks = '\\`\\`\\`';

let result = '';
let allowed = true;
let quotesOpened = false;
let buffer = '';

for (let i = 0; i < s.length; i += 1) {
const char = s[i];
const chain = s.substring(i, i + 3);
if (char === '\n' && quotesOpened) {
allowed = false;
}
if (chain === tripleBackticks) {
if (quotesOpened) {
quotesOpened = false;
if (allowed) {
result += `${tripleBackticks}${buffer}${tripleBackticks}`;
} else {
result += `${escapedTripleBackticks}${buffer}${escapedTripleBackticks}`;
allowed = true;
}
buffer = '';
} else {
quotesOpened = true;
}
// skip chain
i += 2;
continue;
}
if (quotesOpened) {
buffer += char;
} else {
result += char;
}
}

if (quotesOpened) {
// Handle unclosed code block
result += allowed ? tripleBackticks + buffer : escapedTripleBackticks + buffer;
}

return result;
}

export function unescapeMarkdown(s: string): string {
return (
s
// Unescape markdown
.replace(unescapeMarkdownRegex, '$1')
// Unescape markdown header
.replace(unescapeMarkdownHeaderRegex, '===')
);
}
32 changes: 0 additions & 32 deletions src/system/string.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,38 +130,6 @@ export function encodeHtmlWeak(s: string | undefined): string | undefined {
});
}

const escapeMarkdownRegex = /[\\`*_{}[\]()#+\-.!]/g;
const unescapeMarkdownRegex = /\\([\\`*_{}[\]()#+\-.!])/g;

const escapeMarkdownHeaderRegex = /^===/gm;
const unescapeMarkdownHeaderRegex = /^\u200b===/gm;

// const sampleMarkdown = '## message `not code` *not important* _no underline_ \n> don\'t quote me \n- don\'t list me \n+ don\'t list me \n1. don\'t list me \nnot h1 \n=== \nnot h2 \n---\n***\n---\n___';
const markdownQuotedRegex = /\r?\n/g;

export function escapeMarkdown(s: string, options: { quoted?: boolean } = {}): string {
s = s
// Escape markdown
.replace(escapeMarkdownRegex, '\\$&')
// Escape markdown header (since the above regex won't match it)
.replace(escapeMarkdownHeaderRegex, '\u200b===');

if (!options.quoted) return s;

// Keep under the same block-quote but with line breaks
return s.trim().replace(markdownQuotedRegex, '\t\\\n> ');
}

export function unescapeMarkdown(s: string): string {
return (
s
// Unescape markdown
.replace(unescapeMarkdownRegex, '$1')
// Unescape markdown header
.replace(unescapeMarkdownHeaderRegex, '===')
);
}

export function escapeRegex(s: string) {
return s.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}
Expand Down
3 changes: 3 additions & 0 deletions src/webviews/apps/plus/graph/GraphWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { createCommandLink } from '../../shared/commands';
import { GlButton } from '../../shared/components/button.react';
import { CodeIcon } from '../../shared/components/code-icon.react';
import { GlConnect } from '../../shared/components/integrations/connect.react';
import { GlMarkdown } from '../../shared/components/markdown/markdown.react';
import { MenuDivider, MenuItem, MenuLabel, MenuList } from '../../shared/components/menu/react';
import { PopMenu } from '../../shared/components/overlays/pop-menu/react';
import { GlPopover } from '../../shared/components/overlays/popover.react';
Expand Down Expand Up @@ -1604,6 +1605,8 @@ export function GraphWrapper({
avatarUrlByEmail={avatars}
columnsSettings={columns}
contexts={context}
// @ts-expect-error returnType of formatCommitMessage callback expects to be string, but it works fine with react element
formatCommitMessage={e => <GlMarkdown markdown={e}></GlMarkdown>}
cssVariables={styleProps?.cssVariables}
dimMergeCommits={graphConfig?.dimMergeCommits}
downstreamsByUpstream={downstreams}
Expand Down
2 changes: 1 addition & 1 deletion src/webviews/apps/plus/graph/hover/graphHover.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { debounce } from '../../../../../system/function';
import { getSettledValue, isPromise } from '../../../../../system/promise';
import { GlElement } from '../../../shared/components/element';
import type { GlPopover } from '../../../shared/components/overlays/popover.react';
import '../../../shared/components/markdown/markdown';
import '../../../shared/components/overlays/popover';
import './markdown';

declare global {
interface HTMLElementTagNameMap {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { reactWrapper } from '../helpers/react-wrapper';
import { GlMarkdown as GlMarkdownWC } from './markdown';

export interface GlMarkdown extends GlMarkdownWC {}
export const GlMarkdown = reactWrapper(GlMarkdownWC, { tagName: 'gl-markdown' });
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { marked } from 'marked';
import type { ThemeIcon } from 'vscode';

@customElement('gl-markdown')
export class GLMarkdown extends LitElement {
export class GlMarkdown extends LitElement {
static override styles = css`
a,
a code {
Expand Down
Loading