Merge "Replace gr-formatted-text with gr-markdown"
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
index ed752e6..76ec316 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view.ts
@@ -15,7 +15,7 @@
import '../../shared/gr-change-star/gr-change-star';
import '../../shared/gr-change-status/gr-change-status';
import '../../shared/gr-editable-content/gr-editable-content';
-import '../../shared/gr-markdown/gr-markdown';
+import '../../shared/gr-formatted-text/gr-formatted-text';
import '../../shared/gr-overlay/gr-overlay';
import '../../shared/gr-tooltip-content/gr-tooltip-content';
import '../gr-change-actions/gr-change-actions';
@@ -958,7 +958,7 @@
/* Account for border and padding and rounding errors. */
max-width: calc(72ch + 2px + 2 * var(--spacing-m) + 0.4px);
}
- .commitMessage gr-markdown {
+ .commitMessage gr-formatted-text {
word-break: break-word;
}
#commitMessageEditor {
@@ -1459,9 +1459,10 @@
.commitCollapsible=${this.computeCommitCollapsible()}
remove-zero-width-space=""
>
- <gr-markdown
+ <gr-formatted-text
+ .markdown=${false}
.content=${this.latestCommitMessage ?? ''}
- ></gr-markdown>
+ ></gr-formatted-text>
</gr-editable-content>
</div>
<h3 class="assistive-tech-only">Comments and Checks Summary</h3>
diff --git a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
index 49248ea..e0c09e2 100644
--- a/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-change-view/gr-change-view_test.ts
@@ -433,7 +433,7 @@
id="commitMessageEditor"
remove-zero-width-space=""
>
- <gr-markdown></gr-markdown>
+ <gr-formatted-text></gr-formatted-text>
</gr-editable-content>
</div>
<h3 class="assistive-tech-only">
diff --git a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
index e11822f..a4da747 100644
--- a/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
+++ b/polygerrit-ui/app/elements/change/gr-message/gr-message.ts
@@ -10,15 +10,13 @@
import '../../shared/gr-date-formatter/gr-date-formatter';
import '../../shared/gr-formatted-text/gr-formatted-text';
import '../gr-message-scores/gr-message-scores';
-import {css, html, LitElement, nothing, PropertyValues} from 'lit';
+import {css, html, LitElement, nothing} from 'lit';
import {MessageTag, SpecialFilePath} from '../../../constants/constants';
import {customElement, property, state} from 'lit/decorators.js';
import {hasOwnProperty} from '../../../utils/common-util';
import {
ChangeInfo,
ServerInfo,
- ConfigInfo,
- RepoName,
ReviewInputTag,
NumericChangeId,
ChangeMessageId,
@@ -105,9 +103,6 @@
@property({type: Boolean})
hideAutomated = false;
- @property({type: String})
- projectName?: RepoName;
-
/**
* A mapping from label names to objects representing the minimum and
* maximum possible values for that label.
@@ -115,9 +110,6 @@
@property({type: Object})
labelExtremes?: LabelExtreme;
- @state()
- private projectConfig?: ConfigInfo;
-
@property({type: Boolean})
loggedIn = false;
@@ -317,12 +309,6 @@
];
}
- override willUpdate(changedProperties: PropertyValues) {
- if (changedProperties.has('projectName')) {
- this.projectNameChanged();
- }
- }
-
override render() {
if (!this.message) return nothing;
if (this.hideAutomated && this.computeIsAutomated()) return nothing;
@@ -437,10 +423,9 @@
);
return html`
<gr-formatted-text
- noTrailingMargin
class="message hideOnCollapsed"
+ .markdown=${true}
.content=${messageContentExpanded}
- .config=${this.projectConfig?.commentlinks}
></gr-formatted-text>
${when(messageContentExpanded, () => this.renderActionContainer())}
<gr-thread-list
@@ -804,16 +789,6 @@
});
}
- private projectNameChanged() {
- if (!this.projectName) {
- this.projectConfig = undefined;
- return;
- }
- this.restApiService.getProjectConfig(this.projectName).then(config => {
- this.projectConfig = config;
- });
- }
-
private computeExpandToggleIcon() {
return this.message?.expanded ? 'expand_less' : 'expand_more';
}
diff --git a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
index da775f6..97b20dc 100644
--- a/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
+++ b/polygerrit-ui/app/elements/change/gr-messages-list/gr-messages-list.ts
@@ -18,7 +18,6 @@
LabelNameToInfoMap,
NumericChangeId,
PatchSetNum,
- RepoName,
VotingRangeInfo,
} from '../../../types/common';
import {CommentThread, isRobot} from '../../../utils/comment-util';
@@ -314,9 +313,6 @@
private commentThreads: CommentThread[] = [];
@state()
- private projectName?: RepoName;
-
- @state()
expandAllState = ExpandAllState.EXPAND_ALL;
// Private but used in tests.
@@ -353,13 +349,6 @@
);
subscribe(
this,
- () => this.changeModel().repo$,
- x => {
- this.projectName = x;
- }
- );
- subscribe(
- this,
() => this.changeModel().changeNum$,
x => {
this.changeNum = x;
@@ -408,7 +397,6 @@
.changeNum=${this.changeNum}
.message=${message}
.commentThreads=${message.commentThreads}
- .projectName=${this.projectName}
@message-anchor-tap=${this.handleAnchorClick}
.labelExtremes=${labelExtremes}
data-message-id=${ifDefined(getMessageId(message) as String)}
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
index 37df7ec..7fe070c 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard.ts
@@ -189,7 +189,7 @@
</div>
<div class="sectionContent">
<gr-formatted-text
- noTrailingMargin
+ .markdown=${true}
.content=${description}
></gr-formatted-text>
</div>
diff --git a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
index 9f44884..2117ec5 100644
--- a/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
+++ b/polygerrit-ui/app/elements/change/gr-submit-requirement-hovercard/gr-submit-requirement-hovercard_test.ts
@@ -223,7 +223,7 @@
<gr-icon icon="description"></gr-icon>
</div>
<div class="sectionContent">
- <gr-formatted-text notrailingmargin=""></gr-formatted-text>
+ <gr-formatted-text></gr-formatted-text>
</div>
</div>
<div class="button">
diff --git a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
index bd52a33..db49e8d 100644
--- a/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
+++ b/polygerrit-ui/app/elements/change/gr-trigger-vote-hovercard/gr-trigger-vote-hovercard.ts
@@ -89,7 +89,7 @@
</div>
<div class="sectionContent">
<gr-formatted-text
- noTrailingMargin
+ .markdown=${true}
.content=${description}
></gr-formatted-text>
</div>
diff --git a/polygerrit-ui/app/elements/checks/gr-checks-results.ts b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
index b2636c8..26fa0cb 100644
--- a/polygerrit-ui/app/elements/checks/gr-checks-results.ts
+++ b/polygerrit-ui/app/elements/checks/gr-checks-results.ts
@@ -54,11 +54,7 @@
import {charsOnly} from '../../utils/string-util';
import {isAttemptSelected, matches} from './gr-checks-util';
import {ChecksTabState, ValueChangedEvent} from '../../types/events';
-import {
- ConfigInfo,
- LabelNameToInfoMap,
- PatchSetNumber,
-} from '../../types/common';
+import {LabelNameToInfoMap, PatchSetNumber} from '../../types/common';
import {spinnerStyles} from '../../styles/gr-spinner-styles';
import {
getLabelStatus,
@@ -70,7 +66,6 @@
import {fontStyles} from '../../styles/gr-font-styles';
import {fire} from '../../utils/event-util';
import {resolve} from '../../models/dependency';
-import {configModelToken} from '../../models/config/config-model';
import {checksModelToken} from '../../models/checks/checks-model';
import {Interaction} from '../../constants/reporting';
import {Deduping} from '../../api/reporting';
@@ -627,13 +622,8 @@
@property({type: Boolean})
hideCodePointers = false;
- @state()
- repoConfig?: ConfigInfo;
-
private getChangeModel = resolve(this, changeModelToken);
- private getConfigModel = resolve(this, configModelToken);
-
static override get styles() {
return [
sharedStyles,
@@ -655,15 +645,6 @@
];
}
- constructor() {
- super();
- subscribe(
- this,
- () => this.getConfigModel().repoConfig$,
- x => (this.repoConfig = x)
- );
- }
-
override render() {
if (!this.result) return '';
return html`
@@ -679,10 +660,9 @@
.value=${this.result}
></gr-endpoint-param>
<gr-formatted-text
- noTrailingMargin
class="message"
- .content=${this.result.message}
- .config=${this.repoConfig?.commentlinks}
+ .markdown=${true}
+ .content=${this.result.message ?? ''}
></gr-formatted-text>
</gr-endpoint-decorator>
`;
diff --git a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
index cc76150..6f40254 100644
--- a/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
+++ b/polygerrit-ui/app/elements/shared/gr-comment/gr-comment.ts
@@ -24,7 +24,6 @@
import {GrOverlay} from '../gr-overlay/gr-overlay';
import {
AccountDetailInfo,
- CommentLinks,
NumericChangeId,
RepoName,
RobotCommentInfo,
@@ -60,7 +59,6 @@
import {CommentSide, SpecialFilePath} from '../../../constants/constants';
import {Subject} from 'rxjs';
import {debounceTime} from 'rxjs/operators';
-import {configModelToken} from '../../../models/config/config-model';
import {changeModelToken} from '../../../models/change/change-model';
import {Interaction} from '../../../constants/reporting';
import {KnownExperimentId} from '../../../services/flags/flags';
@@ -192,9 +190,6 @@
editing = false;
@state()
- commentLinks: CommentLinks = {};
-
- @state()
repoName?: RepoName;
/* The 'dirty' state of the comment.message, which will be saved on demand. */
@@ -239,8 +234,6 @@
private readonly userModel = getAppContext().userModel;
- private readonly configModel = resolve(this, configModelToken);
-
private readonly shortcuts = new ShortcutController(this);
/**
@@ -289,11 +282,6 @@
}
subscribe(
this,
- () => this.configModel().repoCommentLinks$,
- x => (this.commentLinks = x)
- );
- subscribe(
- this,
() => this.userModel.account$,
x => (this.account = x)
);
@@ -724,8 +712,8 @@
gr-diff-selection.-->
<gr-formatted-text
class="message"
- .content=${this.comment?.message}
- .config=${this.commentLinks}
+ .markdown=${true}
+ .content=${this.comment?.message ?? ''}
></gr-formatted-text>
`;
}
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
index 4fee574..bd01af9 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text.ts
@@ -1,59 +1,205 @@
/**
* @license
- * Copyright 2016 Google LLC
+ * Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import '../gr-markdown/gr-markdown';
-import {CommentLinks} from '../../../types/common';
-import {LitElement, css, html, TemplateResult} from 'lit';
-import {customElement, property} from 'lit/decorators.js';
+import {css, html, LitElement} from 'lit';
+import {customElement, property, state} from 'lit/decorators.js';
+import {
+ htmlEscape,
+ sanitizeHtml,
+ sanitizeHtmlToFragment,
+} from '../../../utils/inner-html-util';
+import {unescapeHTML} from '../../../utils/syntax-util';
+import '@polymer/marked-element';
+import {resolve} from '../../../models/dependency';
+import {subscribe} from '../../lit/subscription-controller';
+import {configModelToken} from '../../../models/config/config-model';
+import {CommentLinks} from '../../../api/rest-api';
+import {
+ applyHtmlRewritesFromConfig,
+ applyLinkRewritesFromConfig,
+ linkifyNormalUrls,
+} from '../../../utils/link-util';
-const CODE_MARKER_PATTERN = /^(`{1,3})([^`]+?)\1$/;
-const INLINE_PATTERN = /(\[.+?\]\(.+?\)|`[^`]+?`)/;
-const EXTRACT_LINK_PATTERN = /\[(.+?)\]\((.+?)\)/;
+/**
+ * This element optionally renders markdown and also applies some regex
+ * replacements to linkify key parts of the text defined by the host's config.
+ */
+@customElement('gr-formatted-text')
+export class GrFormattedText extends LitElement {
+ @property({type: String})
+ content = '';
-export type Block = ListBlock | QuoteBlock | Paragraph | CodeBlock | PreBlock;
-export interface ListBlock {
- type: 'list';
- items: ListItem[];
-}
-export interface ListItem {
- spans: InlineItem[];
-}
+ @property({type: Boolean})
+ markdown = false;
-export interface QuoteBlock {
- type: 'quote';
- blocks: Block[];
-}
-export interface Paragraph {
- type: 'paragraph';
- spans: InlineItem[];
-}
-export interface CodeBlock {
- type: 'code';
- text: string;
-}
-export interface PreBlock {
- type: 'pre';
- text: string;
-}
+ @state()
+ private repoCommentLinks: CommentLinks = {};
-export type InlineItem = TextSpan | LinkSpan | CodeSpan;
+ private readonly getConfigModel = resolve(this, configModelToken);
-export interface TextSpan {
- type: 'text';
- text: string;
-}
+ /**
+ * Note: Do not use sharedStyles or other styles here that should not affect
+ * the generated HTML of the markdown.
+ */
+ static override styles = [
+ css`
+ a {
+ color: var(--link-color);
+ }
+ p,
+ ul,
+ code,
+ blockquote {
+ margin: 0 0 var(--spacing-m) 0;
+ max-width: var(--gr-formatted-text-prose-max-width, none);
+ }
+ p:last-child,
+ ul:last-child,
+ blockquote:last-child,
+ pre:last-child {
+ margin: 0;
+ }
+ blockquote {
+ border-left: var(--spacing-xxs) solid var(--comment-quote-marker-color);
+ padding: 0 var(--spacing-m);
+ }
+ code {
+ background-color: var(--background-color-secondary);
+ border: var(--spacing-xxs) solid var(--border-color);
+ display: block;
+ font-family: var(--monospace-font-family);
+ font-size: var(--font-size-code);
+ line-height: var(--line-height-mono);
+ margin: var(--spacing-m) 0;
+ padding: var(--spacing-xxs) var(--spacing-s);
+ overflow-x: auto;
+ /* Pre will preserve whitespace and line breaks but not wrap */
+ white-space: pre;
+ }
+ /* Non-multiline code elements need display:inline to shrink and not take
+ a whole row */
+ :not(pre) > code {
+ display: inline;
+ }
+ p {
+ /* prose will automatically wrap but inline <code> blocks won't and we
+ should overflow in that case rather than wrapping or leaking out */
+ overflow-x: auto;
+ }
+ li {
+ margin-left: var(--spacing-xl);
+ }
+ .plaintext {
+ font: inherit;
+ white-space: var(--linked-text-white-space, pre-wrap);
+ word-wrap: var(--linked-text-word-wrap, break-word);
+ }
+ `,
+ ];
-export interface LinkSpan {
- type: 'link';
- text: string;
- url: string;
-}
+ constructor() {
+ super();
+ subscribe(
+ this,
+ () => this.getConfigModel().repoCommentLinks$,
+ repoCommentLinks => (this.repoCommentLinks = repoCommentLinks)
+ );
+ }
-export interface CodeSpan {
- type: 'code';
- text: string;
+ override render() {
+ if (this.markdown) {
+ return this.renderAsMarkdown();
+ } else {
+ return this.renderAsPlaintext();
+ }
+ }
+
+ private renderAsPlaintext() {
+ const linkedText = this.rewriteText(
+ htmlEscape(this.content).toString(),
+ this.repoCommentLinks
+ );
+
+ return html`
+ <pre class="plaintext">${sanitizeHtmlToFragment(linkedText)}</pre>
+ `;
+ }
+
+ private renderAsMarkdown() {
+ // <marked-element> internals will be in charge of calling our custom
+ // renderer so we wrap 'this.rewriteText' so that 'this' is preserved via
+ // closure.
+ const boundRewriteText = (text: string) =>
+ this.rewriteText(text, this.repoCommentLinks);
+
+ // We are overriding some marked-element renderers for a few reasons:
+ // 1. Disable inline images as a design/policy choice.
+ // 2. Inline code blocks ("codespan") do not unescape HTML characters when
+ // rendering without <pre> and so we must do this manually.
+ // <marked-element> is already escaping these internally. See test
+ // covering this.
+ // 3. Multiline code blocks ("code") is similarly handling escaped
+ // characters using <pre>. The convention is to only use <pre> for multi-
+ // line code blocks so it is not used for inline code blocks. See test
+ // for this.
+ // 4. Rewrite plain text ("text") to apply linking and other config-based
+ // rewrites. Text within code blocks is not passed here.
+ function customRenderer(renderer: {[type: string]: Function}) {
+ renderer['image'] = (href: string, _title: string, text: string) =>
+ `![${text}](${href})`;
+ renderer['codespan'] = (text: string) =>
+ `<code>${unescapeHTML(text)}</code>`;
+ renderer['code'] = (text: string) => `<pre><code>${text}</code></pre>`;
+ renderer['text'] = boundRewriteText;
+ }
+
+ // The child with slot is optional but allows us control over the styling.
+ // The `callback` property lets us do a final sanitization of the output
+ // HTML string before it is rendered by `<marked-element>` in case any
+ // rewrites have been abused to attempt an XSS attack.
+ return html`
+ <marked-element
+ .markdown=${this.escapeAllButBlockQuotes(this.content)}
+ .breaks=${true}
+ .renderer=${customRenderer}
+ .callback=${(_error: string | null, contents: string) =>
+ sanitizeHtml(contents)}
+ >
+ <div slot="markdown-html"></div>
+ </marked-element>
+ `;
+ }
+
+ private escapeAllButBlockQuotes(text: string) {
+ // Escaping the message should be done first to make sure user's literal
+ // input does not get rendered without affecting html added in later steps.
+ text = htmlEscape(text).toString();
+ // Unescape block quotes '>'. This is slightly dangerous as '>' can be used
+ // in HTML fragments, but it is insufficient on it's own.
+ text = text.replace(/(^|\n)>/g, '$1>');
+
+ return text;
+ }
+
+ private rewriteText(text: string, repoCommentLinks: CommentLinks) {
+ // Turn universally identifiable URLs into links. Ex: www.google.com. The
+ // markdown library inside marked-element does this too, but is more
+ // conservative and misses some URLs like "google.com" without "www" prefix.
+ text = linkifyNormalUrls(text);
+
+ // Apply the host's config-specific regex replacements to create links. Ex:
+ // link "Bug 12345" to "google.com/bug/12345"
+ text = applyLinkRewritesFromConfig(text, repoCommentLinks);
+
+ // Apply the host's config-specific regex replacements to write arbitrary
+ // html. Most examples seen in the wild are also used for linking but with
+ // finer control over the rendered text. Ex: "Bug 12345" => "#12345"
+ text = applyHtmlRewritesFromConfig(text, repoCommentLinks);
+
+ return text;
+ }
}
declare global {
@@ -61,344 +207,3 @@
'gr-formatted-text': GrFormattedText;
}
}
-@customElement('gr-formatted-text')
-export class GrFormattedText extends LitElement {
- @property({type: String})
- content?: string;
-
- @property({type: Object})
- config?: CommentLinks;
-
- @property({type: Boolean, reflect: true})
- noTrailingMargin = false;
-
- static override get styles() {
- return [
- css`
- :host {
- display: block;
- font-family: var(--font-family);
- }
- a {
- color: var(--link-color);
- }
- p,
- ul,
- code,
- blockquote,
- gr-markdown.pre {
- margin: 0 0 var(--spacing-m) 0;
- }
- p,
- ul,
- code,
- blockquote {
- max-width: var(--gr-formatted-text-prose-max-width, none);
- }
- :host([noTrailingMargin]) p:last-child,
- :host([noTrailingMargin]) ul:last-child,
- :host([noTrailingMargin]) blockquote:last-child,
- :host([noTrailingMargin]) gr-markdown.pre:last-child {
- margin: 0;
- }
- blockquote {
- border-left: 1px solid #aaa;
- padding: 0 var(--spacing-m);
- }
- code {
- display: block;
- /* pre will preserve whitespace and linebreaks but not wrap */
- white-space: pre;
- background-color: var(--background-color-secondary);
- border: 1px solid var(--border-color);
- border-left-width: var(--spacing-s);
- margin: var(--spacing-m) 0;
- padding: var(--spacing-s) var(--spacing-m);
- overflow-x: auto;
- }
- li {
- list-style-type: disc;
- margin-left: var(--spacing-xl);
- }
- .inline-code,
- code {
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-code);
- line-height: var(--line-height-mono);
- background-color: var(--background-color-secondary);
- border: 1px solid var(--border-color);
- padding: 1px var(--spacing-s);
- }
- `,
- ];
- }
-
- override render() {
- if (!this.content) return;
-
- return html`<gr-markdown
- .markdown=${true}
- .content=${this.content}
- ></gr-markdown>`;
- }
-
- /**
- * Given a source string, parse into an array of block objects. Each block
- * has a `type` property which takes any of the following values.
- * * 'paragraph' (Paragraph of regular text)
- * * 'quote' (Block quote.)
- * * 'pre' (Pre-formatted text.)
- * * 'list' (Unordered list.)
- * * 'code' (code blocks.)
- *
- * For blocks of type 'paragraph' there is a list of spans that is the content
- * for that paragraph.
- *
- * For blocks of type 'pre' and 'code' there is a `text`
- * property that maps to a string of the block's content.
- *
- * For blocks of type 'list', there is an `items` property that maps to a
- * list of strings representing the list items.
- *
- * For blocks of type 'quote', there is a `blocks` property that maps to a
- * list of blocks contained in the quote.
- *
- * NOTE: Strings appearing in all block objects are NOT escaped.
- */
- _computeBlocks(content: string): Block[] {
- const result: Block[] = [];
- const lines = content.replace(/[\s\n\r\t]+$/g, '').split('\n');
-
- for (let i = 0; i < lines.length; i++) {
- if (!lines[i].length) {
- continue;
- }
-
- if (this.isCodeMarkLine(lines[i])) {
- const startOfCode = i + 1;
- const endOfCode = this.getEndOfSection(
- lines,
- startOfCode,
- line => !this.isCodeMarkLine(line)
- );
- result.push({
- type: 'code',
- // Does not include either of the ``` lines
- text: lines.slice(startOfCode, endOfCode).join('\n'),
- });
- i = endOfCode; // advances past the closing```
- continue;
- }
- if (this.isSingleLineCode(lines[i])) {
- // no guard check as _isSingleLineCode tested on the pattern
- const codeContent = lines[i].match(CODE_MARKER_PATTERN)![2];
- result.push({type: 'code', text: codeContent});
- } else if (this.isList(lines[i])) {
- const endOfList = this.getEndOfSection(lines, i + 1, line =>
- this.isList(line)
- );
- result.push(this.makeList(lines.slice(i, endOfList)));
- i = endOfList - 1;
- } else if (this.isQuote(lines[i])) {
- const endOfQuote = this.getEndOfSection(lines, i + 1, line =>
- this.isQuote(line)
- );
- const blockLines = lines
- .slice(i, endOfQuote)
- .map(l => l.replace(/^[ ]?>[ ]?/, ''));
- result.push({
- type: 'quote',
- blocks: this._computeBlocks(blockLines.join('\n')),
- });
- i = endOfQuote - 1;
- } else if (this.isPreFormat(lines[i])) {
- const endOfPre = this.findEndOfPreBlock(lines, i);
- result.push({
- type: 'pre',
- text: lines.slice(i, endOfPre).join('\n'),
- });
- i = endOfPre - 1;
- } else {
- const endOfRegularLines = this.getEndOfSection(lines, i + 1, line =>
- this.isRegularLine(line)
- );
- result.push({
- type: 'paragraph',
- spans: this.computeInlineItems(
- lines.slice(i, endOfRegularLines).join('\n')
- ),
- });
- i = endOfRegularLines - 1;
- }
- }
-
- return result;
- }
-
- private computeInlineItems(content: string): InlineItem[] {
- const result: InlineItem[] = [];
- const textSpans = content.split(INLINE_PATTERN);
- for (let i = 0; i < textSpans.length; ++i) {
- // Because INLINE_PATTERN has a single capturing group, string.split will
- // return strings before and after each match as well as the matched
- // group. These are always interleaved starting with a non-matched string
- // which may be empty.
- if (textSpans[i].length === 0) {
- // No point in processing empty strings.
- continue;
- } else if (i % 2 === 0) {
- // A non-matched string.
- result.push({type: 'text', text: textSpans[i]});
- } else if (textSpans[i].startsWith('`')) {
- result.push({type: 'code', text: textSpans[i].slice(1, -1)});
- } else {
- const m = textSpans[i].match(EXTRACT_LINK_PATTERN);
- if (!m) {
- result.push({type: 'text', text: textSpans[i]});
- } else {
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
- const [_, text, url] = m;
- result.push({type: 'link', text, url});
- }
- }
- }
- return result;
- }
-
- private getEndOfSection(
- lines: string[],
- startIndex: number,
- sectionPredicate: (line: string) => boolean
- ) {
- const index = lines
- .slice(startIndex)
- .findIndex(line => !sectionPredicate(line));
- return index === -1 ? lines.length : index + startIndex;
- }
-
- private findEndOfPreBlock(lines: string[], startIndex: number) {
- let lastPreFormat = startIndex;
- for (let i = startIndex + 1; i < lines.length; ++i) {
- const line = lines[i];
- if (this.isPreFormat(line)) {
- lastPreFormat = i;
- } else if (!this.isWhitespaceLine(line) && line.length !== 0) {
- break;
- }
- }
- return lastPreFormat + 1;
- }
-
- /**
- * Take a block of comment text that contains a list, generate appropriate
- * block objects and append them to the output list.
- *
- * * Item one.
- * * Item two.
- * * item three.
- *
- * TODO(taoalpha): maybe we should also support nested list
- *
- * @param lines The block containing the list.
- */
- private makeList(lines: string[]): Block {
- return {
- type: 'list',
- items: lines.map(line => {
- return {
- spans: this.computeInlineItems(line.substring(1).trim()),
- };
- }),
- };
- }
-
- private isRegularLine(line: string): boolean {
- return (
- !this.isQuote(line) &&
- !this.isCodeMarkLine(line) &&
- !this.isSingleLineCode(line) &&
- !this.isList(line) &&
- !this.isPreFormat(line)
- );
- }
-
- private isQuote(line: string): boolean {
- return line.startsWith('> ') || line.startsWith(' > ');
- }
-
- private isCodeMarkLine(line: string): boolean {
- return /^\s{0,3}```/.test(line);
- }
-
- private isSingleLineCode(line: string): boolean {
- return CODE_MARKER_PATTERN.test(line);
- }
-
- private isPreFormat(line: string): boolean {
- return /^(\s{4}|\t)/.test(line) && !this.isWhitespaceLine(line);
- }
-
- private isList(line: string): boolean {
- return /^[-*] /.test(line);
- }
-
- private isWhitespaceLine(line: string): boolean {
- return /^\s+$/.test(line);
- }
-
- private renderInlineText(content: string): TemplateResult {
- return html`<gr-markdown .content=${content}></gr-markdown>`;
- }
-
- private renderLink(text: string, url: string): TemplateResult {
- return html`<a target="_blank" href=${url}>${text}</a>`;
- }
-
- private renderInlineCode(text: string): TemplateResult {
- return html`<span class="inline-code">${text}</span>`;
- }
-
- private renderInlineItem(span: InlineItem): TemplateResult {
- switch (span.type) {
- case 'text':
- return this.renderInlineText(span.text);
- case 'link':
- return this.renderLink(span.text, span.url);
- case 'code':
- return this.renderInlineCode(span.text);
- default:
- return html``;
- }
- }
-
- private renderListItem(item: ListItem): TemplateResult {
- return html` <li>
- ${item.spans.map(item => this.renderInlineItem(item))}
- </li>`;
- }
-
- private renderBlock(block: Block): TemplateResult {
- switch (block.type) {
- case 'paragraph':
- return html` <p>
- ${block.spans.map(item => this.renderInlineItem(item))}
- </p>`;
- case 'quote':
- return html`
- <blockquote>
- ${block.blocks.map(subBlock => this.renderBlock(subBlock))}
- </blockquote>
- `;
- case 'code':
- return html`<code>${block.text}</code>`;
- case 'pre':
- return html`<pre><code>${block.text}</code></pre>`;
- case 'list':
- return html`
- <ul>
- ${block.items.map(item => this.renderListItem(item))}
- </ul>
- `;
- }
- }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
index a3eff8e..dcd13bd 100644
--- a/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
+++ b/polygerrit-ui/app/elements/shared/gr-formatted-text/gr-formatted-text_test.ts
@@ -1,519 +1,383 @@
/**
* @license
- * Copyright 2016 Google LLC
+ * Copyright 2022 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
-import {fixture, html, assert} from '@open-wc/testing';
import '../../../test/common-test-setup';
-import './gr-formatted-text';
+import {assert, fixture, html} from '@open-wc/testing';
+import {changeModelToken} from '../../../models/change/change-model';
import {
- GrFormattedText,
- Block,
- ListBlock,
- Paragraph,
- QuoteBlock,
- PreBlock,
- CodeBlock,
- InlineItem,
- ListItem,
- TextSpan,
- LinkSpan,
-} from './gr-formatted-text';
+ ConfigModel,
+ configModelToken,
+} from '../../../models/config/config-model';
+import {wrapInProvider} from '../../../models/di-provider-element';
+import {getAppContext} from '../../../services/app-context';
+import './gr-formatted-text';
+import {GrFormattedText} from './gr-formatted-text';
+import {createConfig} from '../../../test/test-data-generators';
+import {waitUntilObserved} from '../../../test/test-utils';
+import {CommentLinks} from '../../../api/rest-api';
+import {testResolver} from '../../../test/common-test-setup';
suite('gr-formatted-text tests', () => {
let element: GrFormattedText;
+ let configModel: ConfigModel;
- function assertSpan(actual: InlineItem, expected: InlineItem) {
- assert.equal(actual.type, expected.type);
- assert.equal(actual.text, expected.text);
- switch (actual.type) {
- case 'link':
- assert.equal(actual.url, (expected as LinkSpan).url);
- break;
- }
- }
-
- function assertTextBlock(block: Block, spans: InlineItem[]) {
- assert.equal(block.type, 'paragraph');
- const paragraph = block as Paragraph;
- assert.equal(paragraph.spans.length, spans.length);
- for (let i = 0; i < paragraph.spans.length; ++i) {
- assertSpan(paragraph.spans[i], spans[i]);
- }
- }
-
- function assertPreBlock(block: Block, text: string) {
- assert.equal(block.type, 'pre');
- const preBlock = block as PreBlock;
- assert.equal(preBlock.text, text);
- }
-
- function assertCodeBlock(block: Block, text: string) {
- assert.equal(block.type, 'code');
- const preBlock = block as CodeBlock;
- assert.equal(preBlock.text, text);
- }
-
- function assertSimpleTextBlock(block: Block, text: string) {
- assertTextBlock(block, [{type: 'text', text}]);
- }
-
- function assertListBlock(block: Block, items: ListItem[]) {
- assert.equal(block.type, 'list');
- const listBlock = block as ListBlock;
- assert.deepEqual(listBlock.items, items);
- }
-
- function assertQuoteBlock(block: Block): QuoteBlock {
- assert.equal(block.type, 'quote');
- return block as QuoteBlock;
+ async function setCommentLinks(commentlinks: CommentLinks) {
+ configModel.updateRepoConfig({...createConfig(), commentlinks});
+ await waitUntilObserved(
+ configModel.repoCommentLinks$,
+ links => links === commentlinks
+ );
}
setup(async () => {
- element = await fixture(html`<gr-formatted-text></gr-formatted-text>`);
- });
-
- test('parse empty', () => {
- assert.lengthOf(element._computeBlocks(''), 0);
- });
-
- test('render', async () => {
- element.content = 'text `code`';
- await element.updateComplete;
-
- assert.shadowDom.equal(element, /* HTML */ ` <gr-markdown></gr-markdown> `);
- });
-
- for (const text of [
- 'Para1',
- 'Para 1\nStill para 1',
- 'Para 1\n\nPara 2\n\nPara 3',
- ]) {
- test('parse simple', () => {
- const comment = {type: 'text', text} as TextSpan;
- const result = element._computeBlocks(text);
- assert.lengthOf(result, 1);
- assertTextBlock(result[0], [comment]);
- });
- }
-
- test('parse link', () => {
- const comment = '[text](url)';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertTextBlock(result[0], [{type: 'link', text: 'text', url: 'url'}]);
- });
-
- test('parse inline code', () => {
- const comment = 'text `code`';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertTextBlock(result[0], [
- {type: 'text', text: 'text '},
- {type: 'code', text: 'code'},
- ]);
- });
-
- test('parse quote', () => {
- const comment = '> Quote text';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- const quoteBlock = assertQuoteBlock(result[0]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(quoteBlock.blocks[0], 'Quote text');
- });
-
- test('parse quote lead space', () => {
- const comment = ' > Quote text';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- const quoteBlock = assertQuoteBlock(result[0]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(quoteBlock.blocks[0], 'Quote text');
- });
-
- test('parse multiline quote', () => {
- const comment = '> Quote line 1\n> Quote line 2\n > Quote line 3\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- const quoteBlock = assertQuoteBlock(result[0]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(
- quoteBlock.blocks[0],
- 'Quote line 1\nQuote line 2\nQuote line 3'
+ configModel = new ConfigModel(
+ testResolver(changeModelToken),
+ getAppContext().restApiService
);
- });
-
- test('parse pre', () => {
- const comment = ' Four space indent.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertPreBlock(result[0], comment);
- });
-
- test('one space is not a pre', () => {
- const comment = ' One space indent.\n Another line.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertSimpleTextBlock(result[0], comment);
- });
-
- test('parse multi-line space pre', () => {
- const comment = ' One space indent.\n Another line.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertPreBlock(result[0], comment);
- });
-
- test('parse tab pre', () => {
- const comment = '\tOne tab indent.\n\tAnother line.\n Yet another!';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertPreBlock(result[0], comment);
- });
-
- test('parse star list', () => {
- const comment = '* Item 1\n* Item 2\n* Item 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result[0], [
- {spans: [{type: 'text', text: 'Item 1'}]},
- {spans: [{type: 'text', text: 'Item 2'}]},
- {spans: [{type: 'text', text: 'Item 3'}]},
- ]);
- });
-
- test('parse dash list', () => {
- const comment = '- Item 1\n- Item 2\n- Item 3';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result[0], [
- {spans: [{type: 'text', text: 'Item 1'}]},
- {spans: [{type: 'text', text: 'Item 2'}]},
- {spans: [{type: 'text', text: 'Item 3'}]},
- ]);
- });
-
- test('parse mixed list', () => {
- const comment = '- Item 1\n* Item 2\n- Item 3\n* Item 4';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result[0], [
- {spans: [{type: 'text', text: 'Item 1'}]},
- {spans: [{type: 'text', text: 'Item 2'}]},
- {spans: [{type: 'text', text: 'Item 3'}]},
- {spans: [{type: 'text', text: 'Item 4'}]},
- ]);
- });
-
- test('parse mixed block types', () => {
- const comment =
- 'Paragraph\nacross\na\nfew\nlines.' +
- '\n\n' +
- '> Quote\n> across\n> not many lines.' +
- '\n\n' +
- 'Another paragraph' +
- '\n\n' +
- '* Series\n* of\n* list\n* items' +
- '\n\n' +
- 'Yet another paragraph' +
- '\n\n' +
- '\tPreformatted text.' +
- '\n\n' +
- 'Parting words.';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 7);
- assertSimpleTextBlock(result[0], 'Paragraph\nacross\na\nfew\nlines.\n');
-
- const quoteBlock = assertQuoteBlock(result[1]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(
- quoteBlock.blocks[0],
- 'Quote\nacross\nnot many lines.'
- );
-
- assertSimpleTextBlock(result[2], 'Another paragraph\n');
- assertListBlock(result[3], [
- {spans: [{type: 'text', text: 'Series'}]},
- {spans: [{type: 'text', text: 'of'}]},
- {spans: [{type: 'text', text: 'list'}]},
- {spans: [{type: 'text', text: 'items'}]},
- ]);
- assertSimpleTextBlock(result[4], 'Yet another paragraph\n');
- assertPreBlock(result[5], '\tPreformatted text.');
- assertSimpleTextBlock(result[6], 'Parting words.');
- });
-
- test('bullet list 1', () => {
- const comment = 'A\n\n* line 1';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'A\n');
- assertListBlock(result[1], [{spans: [{type: 'text', text: 'line 1'}]}]);
- });
-
- test('bullet list 2', () => {
- const comment = 'A\n\n* line 1\n* 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'A\n');
- assertListBlock(result[1], [
- {spans: [{type: 'text', text: 'line 1'}]},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
- });
-
- test('bullet list 3', () => {
- const comment = 'A\n* line 1\n* 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertSimpleTextBlock(result[0], 'A');
- assertListBlock(result[1], [
- {spans: [{type: 'text', text: 'line 1'}]},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
- assertSimpleTextBlock(result[2], 'B');
- });
-
- test('bullet list 4', () => {
- const comment = '* line 1\n* 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertListBlock(result[0], [
- {spans: [{type: 'text', text: 'line 1'}]},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
- assertSimpleTextBlock(result[1], 'B');
- });
-
- test('bullet list 5', () => {
- const comment =
- 'To see this bug, you have to:\n' +
- '* Be on IMAP or EAS (not on POP)\n' +
- '* Be very unlucky\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'To see this bug, you have to:');
- assertListBlock(result[1], [
- {spans: [{type: 'text', text: 'Be on IMAP or EAS (not on POP)'}]},
- {spans: [{type: 'text', text: 'Be very unlucky'}]},
- ]);
- });
-
- test('bullet list 6', () => {
- const comment =
- 'To see this bug,\n' +
- 'you have to:\n' +
- '* Be on IMAP or EAS (not on POP)\n' +
- '* Be very unlucky\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'To see this bug,\nyou have to:');
- assertListBlock(result[1], [
- {spans: [{type: 'text', text: 'Be on IMAP or EAS (not on POP)'}]},
- {spans: [{type: 'text', text: 'Be very unlucky'}]},
- ]);
- });
-
- test('dash list 1', () => {
- const comment = 'A\n- line 1\n- 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'A');
- assertListBlock(result[1], [
- {spans: [{type: 'text', text: 'line 1'}]},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
- });
-
- test('dash list 2', () => {
- const comment = 'A\n- line 1\n- 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertSimpleTextBlock(result[0], 'A');
- assertListBlock(result[1], [
- {spans: [{type: 'text', text: 'line 1'}]},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
- assertSimpleTextBlock(result[2], 'B');
- });
-
- test('dash list 3', () => {
- const comment = '- line 1\n- 2nd line\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertListBlock(result[0], [
- {spans: [{type: 'text', text: 'line 1'}]},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
- assertSimpleTextBlock(result[1], 'B');
- });
-
- test('list with links', () => {
- const comment = '- [text](http://url)\n- 2nd line';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertListBlock(result[0], [
- {
- spans: [{type: 'link', text: 'text', url: 'http://url'}],
+ await setCommentLinks({
+ customLinkRewrite: {
+ match: '(LinkRewriteMe)',
+ link: 'http://google.com/$1',
},
- {spans: [{type: 'text', text: '2nd line'}]},
- ]);
+ customHtmlRewrite: {
+ match: 'HTMLRewriteMe',
+ html: '<div>HTMLRewritten</div>',
+ },
+ });
+ element = (
+ await fixture(
+ wrapInProvider(
+ html`<gr-formatted-text></gr-formatted-text>`,
+ configModelToken,
+ configModel
+ )
+ )
+ ).querySelector('gr-formatted-text')!;
});
- test('nested list will NOT be recognized', () => {
- // will be rendered as two separate lists
- const comment = '- line 1\n - line with indentation\n- line 2';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertListBlock(result[0], [{spans: [{type: 'text', text: 'line 1'}]}]);
- assertPreBlock(result[1], ' - line with indentation');
- assertListBlock(result[2], [{spans: [{type: 'text', text: 'line 2'}]}]);
+ suite('as plaintext', () => {
+ setup(async () => {
+ element.markdown = false;
+ await element.updateComplete;
+ });
+
+ test('renders text with links and rewrites', async () => {
+ element.content = `text with plain link: google.com
+ \ntext with config link: LinkRewriteMe
+ \ntext with config html: HTMLRewriteMe`;
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <pre class="plaintext">
+ text with plain link:
+ <a href="http://google.com" rel="noopener" target="_blank">
+ google.com
+ </a>
+ text with config link:
+ <a
+ href="http://google.com/LinkRewriteMe"
+ rel="noopener"
+ target="_blank"
+ >
+ LinkRewriteMe
+ </a>
+ text with config html:
+ <div>HTMLRewritten</div>
+ </pre>
+ `
+ );
+ });
+
+ test('does not render typed html', async () => {
+ element.content = 'plain text <div>foo</div>';
+ await element.updateComplete;
+
+ const escapedDiv = '<div>foo</div>';
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `<pre class="plaintext">plain text ${escapedDiv}</pre>`
+ );
+ });
+
+ test('does not render markdown', async () => {
+ element.content = '# A Markdown Heading';
+ await element.updateComplete;
+
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ '<pre class="plaintext"># A Markdown Heading</pre>'
+ );
+ });
});
- test('pre format 1', () => {
- const comment = 'A\n This is pre\n formatted';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'A');
- assertPreBlock(result[1], ' This is pre\n formatted');
- });
+ suite('as markdown', () => {
+ setup(async () => {
+ element.markdown = true;
+ await element.updateComplete;
+ });
+ test('renders text with links and rewrites', async () => {
+ element.content = `text
+ \ntext with plain link: google.com
+ \ntext with config link: LinkRewriteMe
+ \ntext with config html: HTMLRewriteMe`;
+ await element.updateComplete;
- test('pre format 2', () => {
- const comment = 'A\n This is pre\n formatted\n\nbut this is not';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertSimpleTextBlock(result[0], 'A');
- assertPreBlock(result[1], ' This is pre\n formatted');
- assertSimpleTextBlock(result[2], 'but this is not');
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <p>text</p>
+ <p>
+ text with plain link:
+ <a href="http://google.com" rel="noopener" target="_blank">
+ google.com
+ </a>
+ </p>
+ <p>
+ text with config link:
+ <a
+ href="http://google.com/LinkRewriteMe"
+ rel="noopener"
+ target="_blank"
+ >
+ LinkRewriteMe
+ </a>
+ </p>
+ <p>text with config html:</p>
+ <div>HTMLRewritten</div>
+ <p></p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
- test('pre format 3', () => {
- const comment = 'A\n Q\n <R>\n S\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertSimpleTextBlock(result[0], 'A');
- assertPreBlock(result[1], ' Q\n <R>\n S');
- assertSimpleTextBlock(result[2], 'B');
- });
+ test('renders headings with links and rewrites', async () => {
+ element.content = `# h1-heading
+ \n## h2-heading
+ \n### h3-heading
+ \n#### h4-heading
+ \n##### h5-heading
+ \n###### h6-heading
+ \n# heading with plain link: google.com
+ \n# heading with config link: LinkRewriteMe
+ \n# heading with config html: HTMLRewriteMe`;
+ await element.updateComplete;
- test('pre format 4', () => {
- const comment = ' Q\n <R>\n S\n\nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertPreBlock(result[0], ' Q\n <R>\n S');
- assertSimpleTextBlock(result[1], 'B');
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <h1>h1-heading</h1>
+ <h2>h2-heading</h2>
+ <h3>h3-heading</h3>
+ <h4>h4-heading</h4>
+ <h5>h5-heading</h5>
+ <h6>h6-heading</h6>
+ <h1>
+ heading with plain link:
+ <a href="http://google.com" rel="noopener" target="_blank">
+ google.com
+ </a>
+ </h1>
+ <h1>
+ heading with config link:
+ <a
+ href="http://google.com/LinkRewriteMe"
+ rel="noopener"
+ target="_blank"
+ >
+ LinkRewriteMe
+ </a>
+ </h1>
+ <h1>
+ heading with config html:
+ <div>HTMLRewritten</div>
+ </h1>
+ </div>
+ </marked-element>
+ `
+ );
+ });
- test('pre format 5', () => {
- const comment = ' Q\n <R>\n S\n \nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertPreBlock(result[0], ' Q\n <R>\n S');
- assertSimpleTextBlock(result[1], ' \nB');
- });
+ test('renders inline-code without linking or rewriting', async () => {
+ element.content = `\`inline code\`
+ \n\`inline code with plain link: google.com\`
+ \n\`inline code with config link: LinkRewriteMe\`
+ \n\`inline code with config html: HTMLRewriteMe\``;
+ await element.updateComplete;
- test('pre format 6', () => {
- const comment = ' Q\n <R>\n\n S\n \nB';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertPreBlock(result[0], ' Q\n <R>\n\n S');
- assertSimpleTextBlock(result[1], ' \nB');
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <p>
+ <code>inline code</code>
+ </p>
+ <p>
+ <code>inline code with plain link: google.com</code>
+ </p>
+ <p>
+ <code>inline code with config link: LinkRewriteMe</code>
+ </p>
+ <p>
+ <code>inline code with config html: HTMLRewriteMe</code>
+ </p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
+ test('renders multiline-code without linking or rewriting', async () => {
+ element.content = `\`\`\`\nmultiline code\n\`\`\`
+ \n\`\`\`\nmultiline code with plain link: google.com\n\`\`\`
+ \n\`\`\`\nmultiline code with config link: LinkRewriteMe\n\`\`\`
+ \n\`\`\`\nmultiline code with config html: HTMLRewriteMe\n\`\`\``;
+ await element.updateComplete;
- test('quote 1', () => {
- const comment = "> I'm happy with quotes!!";
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- const quoteBlock = assertQuoteBlock(result[0]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(quoteBlock.blocks[0], "I'm happy with quotes!!");
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <pre>
+ <code>multiline code</code>
+ </pre>
+ <pre>
+ <code>multiline code with plain link: google.com</code>
+ </pre>
+ <pre>
+ <code>multiline code with config link: LinkRewriteMe</code>
+ </pre>
+ <pre>
+ <code>multiline code with config html: HTMLRewriteMe</code>
+ </pre>
+ </div>
+ </marked-element>
+ `
+ );
+ });
- test('quote 2', () => {
- const comment = "> I'm happy\n > with quotes!\n\nSee above.";
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- const quoteBlock = assertQuoteBlock(result[0]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(quoteBlock.blocks[0], "I'm happy\nwith quotes!");
- assertSimpleTextBlock(result[1], 'See above.');
- });
+ test('does not render inline images into <img> tags', async () => {
+ element.content = '![img](google.com/img.png)';
+ await element.updateComplete;
- test('quote 3', () => {
- const comment = 'See this said:\n > a quoted\n > string block\n\nOK?';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertSimpleTextBlock(result[0], 'See this said:');
- const quoteBlock = assertQuoteBlock(result[1]);
- assert.lengthOf(quoteBlock.blocks, 1);
- assertSimpleTextBlock(quoteBlock.blocks[0], 'a quoted\nstring block');
- assertSimpleTextBlock(result[2], 'OK?');
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <p>![img](google.com/img.png)</p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
- test('nested quotes', () => {
- const comment = ' > > prior\n > \n > next\n';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- const outerQuoteBlock = assertQuoteBlock(result[0]);
- assert.lengthOf(outerQuoteBlock.blocks, 2);
- const nestedQuoteBlock = assertQuoteBlock(outerQuoteBlock.blocks[0]);
- assert.lengthOf(nestedQuoteBlock.blocks, 1);
- assertSimpleTextBlock(nestedQuoteBlock.blocks[0], 'prior');
- assertSimpleTextBlock(outerQuoteBlock.blocks[1], 'next');
- });
+ test('renders inline links into <a> tags', async () => {
+ element.content = '[myLink](https://www.google.com)';
+ await element.updateComplete;
- test('code entire text', () => {
- const comment = '```\n// test code\n```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assertCodeBlock(result[0], '// test code');
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <p>
+ <a href="https://www.google.com">myLink</a>
+ </p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
- test('code first line is descriptor not part of code', () => {
- const comment = 'test code\n```descr\n// test code\n```\nsomething else';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 3);
- assertSimpleTextBlock(result[0], 'test code');
- // 'descr' is omitted.
- assertCodeBlock(result[1], '// test code');
- assertSimpleTextBlock(result[2], 'something else');
- });
+ test('renders block quotes with links and rewrites', async () => {
+ element.content = `> block quote
+ \n> block quote with plain link: google.com
+ \n> block quote with config link: LinkRewriteMe
+ \n> block quote with config html: HTMLRewriteMe`;
+ await element.updateComplete;
- test('code open without close eats everything', () => {
- const comment = 'test code\n```\n// test code\n// more code';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 2);
- assertSimpleTextBlock(result[0], 'test code');
- assertCodeBlock(result[1], '// test code\n// more code');
- });
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <blockquote>
+ <p>block quote</p>
+ </blockquote>
+ <blockquote>
+ <p>
+ block quote with plain link:
+ <a href="http://google.com" rel="noopener" target="_blank">
+ google.com
+ </a>
+ </p>
+ </blockquote>
+ <blockquote>
+ <p>
+ block quote with config link:
+ <a
+ href="http://google.com/LinkRewriteMe"
+ rel="noopener"
+ target="_blank"
+ >
+ LinkRewriteMe
+ </a>
+ </p>
+ </blockquote>
+ <blockquote>
+ <p>block quote with config html:</p>
+ <div>HTMLRewritten</div>
+ <p></p>
+ </blockquote>
+ </div>
+ </marked-element>
+ `
+ );
+ });
- test('backticks inside line not code', () => {
- const comment = 'test code\nwords ```\n// test code```';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- // We don't care how paragraph itself is parsed for this test.
- assert.equal(result[0].type, 'paragraph');
- });
+ test('never renders typed html', async () => {
+ element.content = `plain text <div>foo</div>
+ \n\`inline code <div>foo</div>\`
+ \n\`\`\`\nmultiline code <div>foo</div>\`\`\`
+ \n> block quote <div>foo</div>
+ \n[inline link <div>foo</div>](http://google.com)`;
+ await element.updateComplete;
- test('mix all 1', () => {
- const comment =
- ' bullets:\n- bullet 1\n- bullet 2\n\ncode example:\n' +
- '```\n// test code\n```\n\n> reference is here';
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 5);
- assert.equal(result[0].type, 'pre');
- assert.equal(result[1].type, 'list');
- assert.equal(result[2].type, 'paragraph');
- assert.equal(result[3].type, 'code');
- assert.equal(result[4].type, 'quote');
- });
-
- test('text with \\t is paragraph', () => {
- const comment =
- "Changes to NoteDb or entities packages require careful consideration. Make sure your change is forward compatible and add the footer 'Forward-Compatible: checked' to your commit message";
- const result = element._computeBlocks(comment);
- assert.lengthOf(result, 1);
- assert.equal(result[0].type, 'paragraph');
+ const escapedDiv = '<div>foo</div>';
+ assert.shadowDom.equal(
+ element,
+ /* HTML */ `
+ <marked-element>
+ <div slot="markdown-html">
+ <p>plain text ${escapedDiv}</p>
+ <p>
+ <code>inline code ${escapedDiv}</code>
+ </p>
+ <pre>
+ <code>
+ multiline code ${escapedDiv}
+ </code>
+ </pre>
+ <blockquote>
+ <p>block quote ${escapedDiv}</p>
+ </blockquote>
+ <p>
+ <a href="http://google.com">inline link ${escapedDiv}</a>
+ </p>
+ </div>
+ </marked-element>
+ `
+ );
+ });
});
});
diff --git a/polygerrit-ui/app/elements/shared/gr-markdown/gr-markdown.ts b/polygerrit-ui/app/elements/shared/gr-markdown/gr-markdown.ts
deleted file mode 100644
index c315603..0000000
--- a/polygerrit-ui/app/elements/shared/gr-markdown/gr-markdown.ts
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import {css, html, LitElement} from 'lit';
-import {customElement, property, state} from 'lit/decorators.js';
-import {
- htmlEscape,
- sanitizeHtml,
- sanitizeHtmlToFragment,
-} from '../../../utils/inner-html-util';
-import {unescapeHTML} from '../../../utils/syntax-util';
-import '@polymer/marked-element';
-import {resolve} from '../../../models/dependency';
-import {subscribe} from '../../lit/subscription-controller';
-import {configModelToken} from '../../../models/config/config-model';
-import {CommentLinks} from '../../../api/rest-api';
-import {
- applyHtmlRewritesFromConfig,
- applyLinkRewritesFromConfig,
- linkifyNormalUrls,
-} from '../../../utils/link-util';
-
-/**
- * This element renders markdown and also applies some regex replacements to
- * linkify key parts of the text defined by the host's config.
- *
- * TODO: Replace gr-formatted-text with this once markdown flag is rolled out.
- */
-@customElement('gr-markdown')
-export class GrMarkdown extends LitElement {
- @property({type: String})
- content = '';
-
- @property({type: Boolean})
- markdown = false;
-
- @state()
- private repoCommentLinks: CommentLinks = {};
-
- private readonly getConfigModel = resolve(this, configModelToken);
-
- /**
- * Note: Do not use sharedStyles or other styles here that should not affect
- * the generated HTML of the markdown.
- */
- static override styles = [
- css`
- a {
- color: var(--link-color);
- }
- p,
- ul,
- code,
- blockquote {
- margin: 0 0 var(--spacing-m) 0;
- max-width: var(--gr-formatted-text-prose-max-width, none);
- }
- p:last-child,
- ul:last-child,
- blockquote:last-child,
- pre:last-child {
- margin: 0;
- }
- blockquote {
- border-left: var(--spacing-xxs) solid var(--comment-quote-marker-color);
- padding: 0 var(--spacing-m);
- }
- code {
- background-color: var(--background-color-secondary);
- border: var(--spacing-xxs) solid var(--border-color);
- display: block;
- font-family: var(--monospace-font-family);
- font-size: var(--font-size-code);
- line-height: var(--line-height-mono);
- margin: var(--spacing-m) 0;
- padding: var(--spacing-xxs) var(--spacing-s);
- overflow-x: auto;
- /* Pre will preserve whitespace and line breaks but not wrap */
- white-space: pre;
- }
- /* Non-multiline code elements need display:inline to shrink and not take
- a whole row */
- :not(pre) > code {
- display: inline;
- }
- p {
- /* prose will automatically wrap but inline <code> blocks won't and we
- should overflow in that case rather than wrapping or leaking out */
- overflow-x: auto;
- }
- li {
- margin-left: var(--spacing-xl);
- }
- .plaintext {
- font: inherit;
- white-space: var(--linked-text-white-space, pre-wrap);
- word-wrap: var(--linked-text-word-wrap, break-word);
- }
- `,
- ];
-
- constructor() {
- super();
- subscribe(
- this,
- () => this.getConfigModel().repoCommentLinks$,
- repoCommentLinks => (this.repoCommentLinks = repoCommentLinks)
- );
- }
-
- override render() {
- if (this.markdown) {
- return this.renderAsMarkdown();
- } else {
- return this.renderAsPlaintext();
- }
- }
-
- private renderAsPlaintext() {
- const linkedText = this.rewriteText(
- htmlEscape(this.content).toString(),
- this.repoCommentLinks
- );
-
- return html`
- <pre class="plaintext">${sanitizeHtmlToFragment(linkedText)}</pre>
- `;
- }
-
- private renderAsMarkdown() {
- // <marked-element> internals will be in charge of calling our custom
- // renderer so we wrap 'this.rewriteText' so that 'this' is preserved via
- // closure.
- const boundRewriteText = (text: string) =>
- this.rewriteText(text, this.repoCommentLinks);
-
- // We are overriding some marked-element renderers for a few reasons:
- // 1. Disable inline images as a design/policy choice.
- // 2. Inline code blocks ("codespan") do not unescape HTML characters when
- // rendering without <pre> and so we must do this manually.
- // <marked-element> is already escaping these internally. See test
- // covering this.
- // 3. Multiline code blocks ("code") is similarly handling escaped
- // characters using <pre>. The convention is to only use <pre> for multi-
- // line code blocks so it is not used for inline code blocks. See test
- // for this.
- // 4. Rewrite plain text ("text") to apply linking and other config-based
- // rewrites. Text within code blocks is not passed here.
- function customRenderer(renderer: {[type: string]: Function}) {
- renderer['image'] = (href: string, _title: string, text: string) =>
- `![${text}](${href})`;
- renderer['codespan'] = (text: string) =>
- `<code>${unescapeHTML(text)}</code>`;
- renderer['code'] = (text: string) => `<pre><code>${text}</code></pre>`;
- renderer['text'] = boundRewriteText;
- }
-
- // The child with slot is optional but allows us control over the styling.
- // The `callback` property lets us do a final sanitization of the output
- // HTML string before it is rendered by `<marked-element>` in case any
- // rewrites have been abused to attempt an XSS attack.
- return html`
- <marked-element
- .markdown=${this.escapeAllButBlockQuotes(this.content)}
- .breaks=${true}
- .renderer=${customRenderer}
- .callback=${(_error: string | null, contents: string) =>
- sanitizeHtml(contents)}
- >
- <div slot="markdown-html"></div>
- </marked-element>
- `;
- }
-
- private escapeAllButBlockQuotes(text: string) {
- // Escaping the message should be done first to make sure user's literal
- // input does not get rendered without affecting html added in later steps.
- text = htmlEscape(text).toString();
- // Unescape block quotes '>'. This is slightly dangerous as '>' can be used
- // in HTML fragments, but it is insufficient on it's own.
- text = text.replace(/(^|\n)>/g, '$1>');
-
- return text;
- }
-
- private rewriteText(text: string, repoCommentLinks: CommentLinks) {
- // Turn universally identifiable URLs into links. Ex: www.google.com. The
- // markdown library inside marked-element does this too, but is more
- // conservative and misses some URLs like "google.com" without "www" prefix.
- text = linkifyNormalUrls(text);
-
- // Apply the host's config-specific regex replacements to create links. Ex:
- // link "Bug 12345" to "google.com/bug/12345"
- text = applyLinkRewritesFromConfig(text, repoCommentLinks);
-
- // Apply the host's config-specific regex replacements to write arbitrary
- // html. Most examples seen in the wild are also used for linking but with
- // finer control over the rendered text. Ex: "Bug 12345" => "#12345"
- text = applyHtmlRewritesFromConfig(text, repoCommentLinks);
-
- return text;
- }
-}
-
-declare global {
- interface HTMLElementTagNameMap {
- 'gr-markdown': GrMarkdown;
- }
-}
diff --git a/polygerrit-ui/app/elements/shared/gr-markdown/gr-markdown_test.ts b/polygerrit-ui/app/elements/shared/gr-markdown/gr-markdown_test.ts
deleted file mode 100644
index 2327dfe..0000000
--- a/polygerrit-ui/app/elements/shared/gr-markdown/gr-markdown_test.ts
+++ /dev/null
@@ -1,383 +0,0 @@
-/**
- * @license
- * Copyright 2022 Google LLC
- * SPDX-License-Identifier: Apache-2.0
- */
-import '../../../test/common-test-setup';
-import {assert, fixture, html} from '@open-wc/testing';
-import {changeModelToken} from '../../../models/change/change-model';
-import {
- ConfigModel,
- configModelToken,
-} from '../../../models/config/config-model';
-import {wrapInProvider} from '../../../models/di-provider-element';
-import {getAppContext} from '../../../services/app-context';
-import './gr-markdown';
-import {GrMarkdown} from './gr-markdown';
-import {createConfig} from '../../../test/test-data-generators';
-import {waitUntilObserved} from '../../../test/test-utils';
-import {CommentLinks} from '../../../api/rest-api';
-import {testResolver} from '../../../test/common-test-setup';
-
-suite('gr-markdown tests', () => {
- let element: GrMarkdown;
- let configModel: ConfigModel;
-
- async function setCommentLinks(commentlinks: CommentLinks) {
- configModel.updateRepoConfig({...createConfig(), commentlinks});
- await waitUntilObserved(
- configModel.repoCommentLinks$,
- links => links === commentlinks
- );
- }
-
- setup(async () => {
- configModel = new ConfigModel(
- testResolver(changeModelToken),
- getAppContext().restApiService
- );
- await setCommentLinks({
- customLinkRewrite: {
- match: '(LinkRewriteMe)',
- link: 'http://google.com/$1',
- },
- customHtmlRewrite: {
- match: 'HTMLRewriteMe',
- html: '<div>HTMLRewritten</div>',
- },
- });
- element = (
- await fixture(
- wrapInProvider(
- html`<gr-markdown></gr-markdown>`,
- configModelToken,
- configModel
- )
- )
- ).querySelector('gr-markdown')!;
- });
-
- suite('as plaintext', () => {
- setup(async () => {
- element.markdown = false;
- await element.updateComplete;
- });
-
- test('renders text with links and rewrites', async () => {
- element.content = `text with plain link: google.com
- \ntext with config link: LinkRewriteMe
- \ntext with config html: HTMLRewriteMe`;
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <pre class="plaintext">
- text with plain link:
- <a href="http://google.com" rel="noopener" target="_blank">
- google.com
- </a>
- text with config link:
- <a
- href="http://google.com/LinkRewriteMe"
- rel="noopener"
- target="_blank"
- >
- LinkRewriteMe
- </a>
- text with config html:
- <div>HTMLRewritten</div>
- </pre>
- `
- );
- });
-
- test('does not render typed html', async () => {
- element.content = 'plain text <div>foo</div>';
- await element.updateComplete;
-
- const escapedDiv = '<div>foo</div>';
- assert.shadowDom.equal(
- element,
- /* HTML */ `<pre class="plaintext">plain text ${escapedDiv}</pre>`
- );
- });
-
- test('does not render markdown', async () => {
- element.content = '# A Markdown Heading';
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ '<pre class="plaintext"># A Markdown Heading</pre>'
- );
- });
- });
-
- suite('as markdown', () => {
- setup(async () => {
- element.markdown = true;
- await element.updateComplete;
- });
- test('renders text with links and rewrites', async () => {
- element.content = `text
- \ntext with plain link: google.com
- \ntext with config link: LinkRewriteMe
- \ntext with config html: HTMLRewriteMe`;
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <p>text</p>
- <p>
- text with plain link:
- <a href="http://google.com" rel="noopener" target="_blank">
- google.com
- </a>
- </p>
- <p>
- text with config link:
- <a
- href="http://google.com/LinkRewriteMe"
- rel="noopener"
- target="_blank"
- >
- LinkRewriteMe
- </a>
- </p>
- <p>text with config html:</p>
- <div>HTMLRewritten</div>
- <p></p>
- </div>
- </marked-element>
- `
- );
- });
-
- test('renders headings with links and rewrites', async () => {
- element.content = `# h1-heading
- \n## h2-heading
- \n### h3-heading
- \n#### h4-heading
- \n##### h5-heading
- \n###### h6-heading
- \n# heading with plain link: google.com
- \n# heading with config link: LinkRewriteMe
- \n# heading with config html: HTMLRewriteMe`;
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <h1>h1-heading</h1>
- <h2>h2-heading</h2>
- <h3>h3-heading</h3>
- <h4>h4-heading</h4>
- <h5>h5-heading</h5>
- <h6>h6-heading</h6>
- <h1>
- heading with plain link:
- <a href="http://google.com" rel="noopener" target="_blank">
- google.com
- </a>
- </h1>
- <h1>
- heading with config link:
- <a
- href="http://google.com/LinkRewriteMe"
- rel="noopener"
- target="_blank"
- >
- LinkRewriteMe
- </a>
- </h1>
- <h1>
- heading with config html:
- <div>HTMLRewritten</div>
- </h1>
- </div>
- </marked-element>
- `
- );
- });
-
- test('renders inline-code without linking or rewriting', async () => {
- element.content = `\`inline code\`
- \n\`inline code with plain link: google.com\`
- \n\`inline code with config link: LinkRewriteMe\`
- \n\`inline code with config html: HTMLRewriteMe\``;
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <p>
- <code>inline code</code>
- </p>
- <p>
- <code>inline code with plain link: google.com</code>
- </p>
- <p>
- <code>inline code with config link: LinkRewriteMe</code>
- </p>
- <p>
- <code>inline code with config html: HTMLRewriteMe</code>
- </p>
- </div>
- </marked-element>
- `
- );
- });
- test('renders multiline-code without linking or rewriting', async () => {
- element.content = `\`\`\`\nmultiline code\n\`\`\`
- \n\`\`\`\nmultiline code with plain link: google.com\n\`\`\`
- \n\`\`\`\nmultiline code with config link: LinkRewriteMe\n\`\`\`
- \n\`\`\`\nmultiline code with config html: HTMLRewriteMe\n\`\`\``;
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <pre>
- <code>multiline code</code>
- </pre>
- <pre>
- <code>multiline code with plain link: google.com</code>
- </pre>
- <pre>
- <code>multiline code with config link: LinkRewriteMe</code>
- </pre>
- <pre>
- <code>multiline code with config html: HTMLRewriteMe</code>
- </pre>
- </div>
- </marked-element>
- `
- );
- });
-
- test('does not render inline images into <img> tags', async () => {
- element.content = '![img](google.com/img.png)';
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <p>![img](google.com/img.png)</p>
- </div>
- </marked-element>
- `
- );
- });
-
- test('renders inline links into <a> tags', async () => {
- element.content = '[myLink](https://www.google.com)';
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <p>
- <a href="https://www.google.com">myLink</a>
- </p>
- </div>
- </marked-element>
- `
- );
- });
-
- test('renders block quotes with links and rewrites', async () => {
- element.content = `> block quote
- \n> block quote with plain link: google.com
- \n> block quote with config link: LinkRewriteMe
- \n> block quote with config html: HTMLRewriteMe`;
- await element.updateComplete;
-
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <blockquote>
- <p>block quote</p>
- </blockquote>
- <blockquote>
- <p>
- block quote with plain link:
- <a href="http://google.com" rel="noopener" target="_blank">
- google.com
- </a>
- </p>
- </blockquote>
- <blockquote>
- <p>
- block quote with config link:
- <a
- href="http://google.com/LinkRewriteMe"
- rel="noopener"
- target="_blank"
- >
- LinkRewriteMe
- </a>
- </p>
- </blockquote>
- <blockquote>
- <p>block quote with config html:</p>
- <div>HTMLRewritten</div>
- <p></p>
- </blockquote>
- </div>
- </marked-element>
- `
- );
- });
-
- test('never renders typed html', async () => {
- element.content = `plain text <div>foo</div>
- \n\`inline code <div>foo</div>\`
- \n\`\`\`\nmultiline code <div>foo</div>\`\`\`
- \n> block quote <div>foo</div>
- \n[inline link <div>foo</div>](http://google.com)`;
- await element.updateComplete;
-
- const escapedDiv = '<div>foo</div>';
- assert.shadowDom.equal(
- element,
- /* HTML */ `
- <marked-element>
- <div slot="markdown-html">
- <p>plain text ${escapedDiv}</p>
- <p>
- <code>inline code ${escapedDiv}</code>
- </p>
- <pre>
- <code>
- multiline code ${escapedDiv}
- </code>
- </pre>
- <blockquote>
- <p>block quote ${escapedDiv}</p>
- </blockquote>
- <p>
- <a href="http://google.com">inline link ${escapedDiv}</a>
- </p>
- </div>
- </marked-element>
- `
- );
- });
- });
-});
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
index 1637296..6caeb62 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-processor/gr-diff-processor_test.ts
@@ -637,8 +637,8 @@
// REST API.
let content = [
' <section class="summary">',
- ' <gr-markdown content="' +
- '[[_computeCurrentRevisionMessage(change)]]"></gr-markdown>',
+ ' <gr-formatted-text content="' +
+ '[[_computeCurrentRevisionMessage(change)]]"></gr-formatted-text>',
' </section>',
];
let highlights = [
@@ -659,13 +659,9 @@
},
{
contentIndex: 1,
+ endIndex: 101,
startIndex: 75,
},
- {
- contentIndex: 2,
- startIndex: 0,
- endIndex: 12,
- },
]);
const lines = element.linesFromRows(
GrDiffLineType.BOTH,
@@ -679,7 +675,7 @@
assert.isTrue(lines[1].hasIntralineInfo);
assert.equal(lines[1].highlights.length, 2);
assert.isTrue(lines[2].hasIntralineInfo);
- assert.equal(lines[2].highlights.length, 1);
+ assert.equal(lines[2].highlights.length, 0);
content = [
' this._path = value.path;',
diff --git a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
index 89a0756..8acaf04 100644
--- a/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
+++ b/polygerrit-ui/app/embed/diff/gr-diff-selection/gr-diff-selection_test.ts
@@ -22,7 +22,9 @@
<div data-side="left">
<div class="comment-thread">
<div class="gr-formatted-text message">
- <span id="output" class="gr-markdown">This is a comment</span>
+ <span id="output" class="gr-formatted-text"
+ >This is a comment</span
+ >
</div>
</div>
</div>
@@ -44,7 +46,7 @@
<div data-side="right">
<div class="comment-thread">
<div class="gr-formatted-text message">
- <span id="output" class="gr-markdown"
+ <span id="output" class="gr-formatted-text"
>This is a comment on the right</span
>
</div>
@@ -60,7 +62,7 @@
<div data-side="left">
<div class="comment-thread">
<div class="gr-formatted-text message">
- <span id="output" class="gr-markdown"
+ <span id="output" class="gr-formatted-text"
>This is <a>a</a> different comment 💩 unicode is fun</span
>
</div>