mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-01 19:15:52 +01:00
add extension point for custom link protocol renderers in markdown (#1639)
This PR allows for custom link protocols to be declared and rendered in markdown.
A new extension point markdown-renderer.link.protocol allows for renderers to hook into the api and implement any custom protocol.
Example:
[description](myprotocol:somelink)
binder.bind("markdown-renderer.link.protocol", { protocol: "myprotocol", renderer: MyProtocolRenderer })
This renderer functions similar to link renderers and receives the href and the description. The latter as the children property.
This PR also fixes two bugs where external- and anchor links were not correctly rendered in pull requests by the review-plugin.
Co-authored-by: Eduard Heimbuch <eduard.heimbuch@cloudogu.com>
This commit is contained in:
committed by
GitHub
parent
8f91c217fc
commit
32b268e6f5
@@ -62,6 +62,17 @@ The following extension points are provided for the frontend:
|
|||||||
- Dynamic extension point for custom language-specific renderers
|
- Dynamic extension point for custom language-specific renderers
|
||||||
- Overrides the default Syntax Highlighter
|
- Overrides the default Syntax Highlighter
|
||||||
- Used by the Markdown Plantuml Plugin
|
- Used by the Markdown Plantuml Plugin
|
||||||
|
### markdown-renderer.link.protocol
|
||||||
|
- Define custom protocols and their renderers for links in markdown
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```markdown
|
||||||
|
[description](myprotocol:somelink)
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
binder.bind("markdown-renderer.link.protocol", { protocol: "myprotocol", renderer: MyProtocolRenderer })
|
||||||
|
```
|
||||||
|
|
||||||
# Deprecated
|
# Deprecated
|
||||||
|
|
||||||
|
|||||||
4
gradle/changelog/link-renderer.yaml
Normal file
4
gradle/changelog/link-renderer.yaml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
- type: added
|
||||||
|
description: Add extension point for custom link protocol renderers in markdown ([#1639](https://github.com/scm-manager/scm-manager/pull/1639))
|
||||||
|
- type: fixed
|
||||||
|
description: External links and anchor links are now correctly rendered in markdown even if no base path is present ([#1639](https://github.com/scm-manager/scm-manager/pull/1639))
|
||||||
@@ -40,6 +40,10 @@ Anchor Links should be rendered a simple a tag with an href: [anchor link](#samp
|
|||||||
|
|
||||||
Links with a protocol other than http should be rendered a simple a tag with an href e.g.: [mail link](mailto:marvin@hitchhiker.com)
|
Links with a protocol other than http should be rendered a simple a tag with an href e.g.: [mail link](mailto:marvin@hitchhiker.com)
|
||||||
|
|
||||||
|
## Custom Protocol
|
||||||
|
|
||||||
|
Renderers for custom protocols can be added via the "markdown-renderer.link.protocol" extension point: [description of scw link](scw:marvin@hitchhiker.com)
|
||||||
|
|
||||||
## Internal
|
## Internal
|
||||||
|
|
||||||
Internal links should be rendered by react-router: [internal link](/buttons)
|
Internal links should be rendered by react-router: [internal link](/buttons)
|
||||||
|
|||||||
@@ -47993,6 +47993,8 @@ exports[`Storyshots MarkdownView Commit Links 1`] = `
|
|||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="https://scm-manager.org/"
|
href="https://scm-manager.org/"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
hitchhiker/heart-of-gold@42
|
hitchhiker/heart-of-gold@42
|
||||||
</a>
|
</a>
|
||||||
@@ -48093,7 +48095,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Headings"
|
href="/#Headings"
|
||||||
>
|
>
|
||||||
Headings
|
Headings
|
||||||
</a>
|
</a>
|
||||||
@@ -48102,7 +48104,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Paragraphs"
|
href="/#Paragraphs"
|
||||||
>
|
>
|
||||||
Paragraphs
|
Paragraphs
|
||||||
</a>
|
</a>
|
||||||
@@ -48111,7 +48113,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Blockquotes"
|
href="/#Blockquotes"
|
||||||
>
|
>
|
||||||
Blockquotes
|
Blockquotes
|
||||||
</a>
|
</a>
|
||||||
@@ -48120,7 +48122,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Lists"
|
href="/#Lists"
|
||||||
>
|
>
|
||||||
Lists
|
Lists
|
||||||
</a>
|
</a>
|
||||||
@@ -48129,7 +48131,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Horizontal"
|
href="/#Horizontal"
|
||||||
>
|
>
|
||||||
Horizontal rule
|
Horizontal rule
|
||||||
</a>
|
</a>
|
||||||
@@ -48138,7 +48140,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Table"
|
href="/#Table"
|
||||||
>
|
>
|
||||||
Table
|
Table
|
||||||
</a>
|
</a>
|
||||||
@@ -48147,7 +48149,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Code"
|
href="/#Code"
|
||||||
>
|
>
|
||||||
Code
|
Code
|
||||||
</a>
|
</a>
|
||||||
@@ -48156,7 +48158,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Inline"
|
href="/#Inline"
|
||||||
>
|
>
|
||||||
Inline elements
|
Inline elements
|
||||||
</a>
|
</a>
|
||||||
@@ -48254,7 +48256,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -48289,7 +48291,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -48362,7 +48364,7 @@ exports[`Storyshots MarkdownView Default 1`] = `
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -48508,7 +48510,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -48541,7 +48543,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -48908,7 +48910,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -50106,7 +50108,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -50135,7 +50137,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
</strong>
|
</strong>
|
||||||
reprehenderit duis
|
reprehenderit duis
|
||||||
<a
|
<a
|
||||||
href="#!"
|
href="/#!"
|
||||||
>
|
>
|
||||||
irure
|
irure
|
||||||
</a>
|
</a>
|
||||||
@@ -50165,7 +50167,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
</code>
|
</code>
|
||||||
anim aute reprehenderit id eu ea. Aute
|
anim aute reprehenderit id eu ea. Aute
|
||||||
<a
|
<a
|
||||||
href="#!"
|
href="/#!"
|
||||||
>
|
>
|
||||||
excepteur proident
|
excepteur proident
|
||||||
</a>
|
</a>
|
||||||
@@ -50204,6 +50206,8 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="https://youtu.be/s6bCmZmy9aQ"
|
href="https://youtu.be/s6bCmZmy9aQ"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Manny Pacquiao"
|
alt="Manny Pacquiao"
|
||||||
@@ -50216,7 +50220,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
<p>
|
<p>
|
||||||
Reprehenderit non eu quis in ad elit esse qui aute id
|
Reprehenderit non eu quis in ad elit esse qui aute id
|
||||||
<a
|
<a
|
||||||
href="#!"
|
href="/#!"
|
||||||
>
|
>
|
||||||
incididunt
|
incididunt
|
||||||
</a>
|
</a>
|
||||||
@@ -58105,6 +58109,166 @@ the story is mostly for checking if the links are rendered correct.
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="custom-protocol"
|
||||||
|
>
|
||||||
|
Custom Protocol
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Renderers for custom protocols can be added via the "markdown-renderer.link.protocol" extension point:
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1px dashed lightgray",
|
||||||
|
"padding": "2px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
Link:
|
||||||
|
marvin@hitchhiker.com
|
||||||
|
[Protocol:
|
||||||
|
scw
|
||||||
|
]
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
children:
|
||||||
|
description of scw link
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="internal"
|
||||||
|
>
|
||||||
|
Internal
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Internal links should be rendered by react-router:
|
||||||
|
<a
|
||||||
|
href="/scm/buttons"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
internal link
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Storyshots MarkdownView Links without Base Path 1`] = `
|
||||||
|
<div
|
||||||
|
className="MarkdownViewstories__Spacing-sc-1lofakk-0 jNfUaE"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="content is-word-break"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
id="links"
|
||||||
|
>
|
||||||
|
Links
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Show case for different style of markdown links.
|
||||||
|
Please note that some of the links may not work in storybook,
|
||||||
|
the story is mostly for checking if the links are rendered correct.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="external"
|
||||||
|
>
|
||||||
|
External
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
External Links should be opened in a new tab:
|
||||||
|
<a
|
||||||
|
href="https://scm-manager.org"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
external link
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="anchor"
|
||||||
|
>
|
||||||
|
Anchor
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Anchor Links should be rendered a simple a tag with an href:
|
||||||
|
<a
|
||||||
|
href="/#sample"
|
||||||
|
>
|
||||||
|
anchor link
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="protocol"
|
||||||
|
>
|
||||||
|
Protocol
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Links with a protocol other than http should be rendered a simple a tag with an href e.g.:
|
||||||
|
<a
|
||||||
|
href="mailto:marvin@hitchhiker.com"
|
||||||
|
>
|
||||||
|
mail link
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h2
|
||||||
|
id="custom-protocol"
|
||||||
|
>
|
||||||
|
Custom Protocol
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Renderers for custom protocols can be added via the "markdown-renderer.link.protocol" extension point:
|
||||||
|
<div
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"border": "1px dashed lightgray",
|
||||||
|
"padding": "2px",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<h4>
|
||||||
|
Link:
|
||||||
|
marvin@hitchhiker.com
|
||||||
|
[Protocol:
|
||||||
|
scw
|
||||||
|
]
|
||||||
|
</h4>
|
||||||
|
<div>
|
||||||
|
children:
|
||||||
|
description of scw link
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
<h2
|
<h2
|
||||||
id="internal"
|
id="internal"
|
||||||
>
|
>
|
||||||
@@ -58116,7 +58280,6 @@ the story is mostly for checking if the links are rendered correct.
|
|||||||
Internal links should be rendered by react-router:
|
Internal links should be rendered by react-router:
|
||||||
<a
|
<a
|
||||||
href="/buttons"
|
href="/buttons"
|
||||||
onClick={[Function]}
|
|
||||||
>
|
>
|
||||||
internal link
|
internal link
|
||||||
</a>
|
</a>
|
||||||
@@ -58146,7 +58309,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Headings"
|
href="/#Headings"
|
||||||
>
|
>
|
||||||
Headings
|
Headings
|
||||||
</a>
|
</a>
|
||||||
@@ -58155,7 +58318,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Paragraphs"
|
href="/#Paragraphs"
|
||||||
>
|
>
|
||||||
Paragraphs
|
Paragraphs
|
||||||
</a>
|
</a>
|
||||||
@@ -58164,7 +58327,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Blockquotes"
|
href="/#Blockquotes"
|
||||||
>
|
>
|
||||||
Blockquotes
|
Blockquotes
|
||||||
</a>
|
</a>
|
||||||
@@ -58173,7 +58336,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Lists"
|
href="/#Lists"
|
||||||
>
|
>
|
||||||
Lists
|
Lists
|
||||||
</a>
|
</a>
|
||||||
@@ -58182,7 +58345,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Horizontal"
|
href="/#Horizontal"
|
||||||
>
|
>
|
||||||
Horizontal rule
|
Horizontal rule
|
||||||
</a>
|
</a>
|
||||||
@@ -58191,7 +58354,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Table"
|
href="/#Table"
|
||||||
>
|
>
|
||||||
Table
|
Table
|
||||||
</a>
|
</a>
|
||||||
@@ -58200,7 +58363,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Code"
|
href="/#Code"
|
||||||
>
|
>
|
||||||
Code
|
Code
|
||||||
</a>
|
</a>
|
||||||
@@ -58209,7 +58372,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href="#Inline"
|
href="/#Inline"
|
||||||
>
|
>
|
||||||
Inline elements
|
Inline elements
|
||||||
</a>
|
</a>
|
||||||
@@ -58303,7 +58466,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -58334,7 +58497,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -58403,7 +58566,7 @@ exports[`Storyshots MarkdownView Skip Html 1`] = `
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -58547,7 +58710,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -58576,7 +58739,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -58986,7 +59149,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -60180,7 +60343,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
|
|
||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="#top"
|
href="/#top"
|
||||||
>
|
>
|
||||||
[Top]
|
[Top]
|
||||||
</a>
|
</a>
|
||||||
@@ -60205,7 +60368,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
</strong>
|
</strong>
|
||||||
reprehenderit duis
|
reprehenderit duis
|
||||||
<a
|
<a
|
||||||
href="#!"
|
href="/#!"
|
||||||
>
|
>
|
||||||
irure
|
irure
|
||||||
</a>
|
</a>
|
||||||
@@ -60235,7 +60398,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
</code>
|
</code>
|
||||||
anim aute reprehenderit id eu ea. Aute
|
anim aute reprehenderit id eu ea. Aute
|
||||||
<a
|
<a
|
||||||
href="#!"
|
href="/#!"
|
||||||
>
|
>
|
||||||
excepteur proident
|
excepteur proident
|
||||||
</a>
|
</a>
|
||||||
@@ -60274,6 +60437,8 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
<p>
|
<p>
|
||||||
<a
|
<a
|
||||||
href="https://youtu.be/s6bCmZmy9aQ"
|
href="https://youtu.be/s6bCmZmy9aQ"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
target="_blank"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
alt="Manny Pacquiao"
|
alt="Manny Pacquiao"
|
||||||
@@ -60286,7 +60451,7 @@ Deserunt officia esse aliquip consectetur duis ut labore laborum commodo aliquip
|
|||||||
<p>
|
<p>
|
||||||
Reprehenderit non eu quis in ad elit esse qui aute id
|
Reprehenderit non eu quis in ad elit esse qui aute id
|
||||||
<a
|
<a
|
||||||
href="#!"
|
href="/#!"
|
||||||
>
|
>
|
||||||
incididunt
|
incididunt
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ export * from "./repos";
|
|||||||
export * from "./table";
|
export * from "./table";
|
||||||
export * from "./toast";
|
export * from "./toast";
|
||||||
export * from "./popover";
|
export * from "./popover";
|
||||||
|
export * from "./markdown/markdownExtensions";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
File,
|
File,
|
||||||
|
|||||||
@@ -57,19 +57,19 @@ describe("test isExternalLink", () => {
|
|||||||
|
|
||||||
describe("test isLinkWithProtocol", () => {
|
describe("test isLinkWithProtocol", () => {
|
||||||
it("should return true", () => {
|
it("should return true", () => {
|
||||||
expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBe(true);
|
expect(isLinkWithProtocol("ldap://[2001:db8::7]/c=GB?objectClass?one")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBe(true);
|
expect(isLinkWithProtocol("mailto:trillian@hitchhiker.com")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBe(true);
|
expect(isLinkWithProtocol("tel:+1-816-555-1212")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBe(true);
|
expect(isLinkWithProtocol("urn:oasis:names:specification:docbook:dtd:xml:4.1.2")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("about:config")).toBe(true);
|
expect(isLinkWithProtocol("about:config")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("http://cloudogu.com")).toBe(true);
|
expect(isLinkWithProtocol("http://cloudogu.com")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("file:///srv/git/project.git")).toBe(true);
|
expect(isLinkWithProtocol("file:///srv/git/project.git")).toBeTruthy();
|
||||||
expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBe(true);
|
expect(isLinkWithProtocol("ssh://trillian@server/project.git")).toBeTruthy();
|
||||||
});
|
});
|
||||||
it("should return false", () => {
|
it("should return false", () => {
|
||||||
expect(isLinkWithProtocol("some/path/link")).toBe(false);
|
expect(isLinkWithProtocol("some/path/link")).toBeFalsy();
|
||||||
expect(isLinkWithProtocol("/some/path/link")).toBe(false);
|
expect(isLinkWithProtocol("/some/path/link")).toBeFalsy();
|
||||||
expect(isLinkWithProtocol("#some-anchor")).toBe(false);
|
expect(isLinkWithProtocol("#some-anchor")).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import React, { FC } from "react";
|
|||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, useLocation } from "react-router-dom";
|
||||||
import ExternalLink from "../navigation/ExternalLink";
|
import ExternalLink from "../navigation/ExternalLink";
|
||||||
import { urls } from "@scm-manager/ui-api";
|
import { urls } from "@scm-manager/ui-api";
|
||||||
|
import { ProtocolLinkRendererExtensionMap } from "./markdownExtensions";
|
||||||
|
|
||||||
const externalLinkRegex = new RegExp("^http(s)?://");
|
const externalLinkRegex = new RegExp("^http(s)?://");
|
||||||
export const isExternalLink = (link: string) => {
|
export const isExternalLink = (link: string) => {
|
||||||
@@ -39,9 +40,10 @@ export const isInternalScmRepoLink = (link: string) => {
|
|||||||
return link.startsWith("/repo/");
|
return link.startsWith("/repo/");
|
||||||
};
|
};
|
||||||
|
|
||||||
const linkWithProtcolRegex = new RegExp("^[a-z]+:");
|
const linkWithProtocolRegex = new RegExp("^([a-z]+):(.+)");
|
||||||
export const isLinkWithProtocol = (link: string) => {
|
export const isLinkWithProtocol = (link: string) => {
|
||||||
return linkWithProtcolRegex.test(link);
|
const match = link.match(linkWithProtocolRegex);
|
||||||
|
return match && { protocol: match[1], link: match[2] };
|
||||||
};
|
};
|
||||||
|
|
||||||
const join = (left: string, right: string) => {
|
const join = (left: string, right: string) => {
|
||||||
@@ -106,10 +108,10 @@ type LinkProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type Props = LinkProps & {
|
type Props = LinkProps & {
|
||||||
base: string;
|
base?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MarkdownLinkRenderer: FC<Props> = ({ href, base, children }) => {
|
const MarkdownLinkRenderer: FC<Props> = ({ href = "", base, children, ...props }) => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
if (isExternalLink(href)) {
|
if (isExternalLink(href)) {
|
||||||
return <ExternalLink to={href}>{children}</ExternalLink>;
|
return <ExternalLink to={href}>{children}</ExternalLink>;
|
||||||
@@ -117,16 +119,39 @@ const MarkdownLinkRenderer: FC<Props> = ({ href, base, children }) => {
|
|||||||
return <a href={href}>{children}</a>;
|
return <a href={href}>{children}</a>;
|
||||||
} else if (isAnchorLink(href)) {
|
} else if (isAnchorLink(href)) {
|
||||||
return <a href={urls.withContextPath(location.pathname) + href}>{children}</a>;
|
return <a href={urls.withContextPath(location.pathname) + href}>{children}</a>;
|
||||||
} else {
|
} else if (base) {
|
||||||
const localLink = createLocalLink(base, location.pathname, href);
|
const localLink = createLocalLink(base, location.pathname, href);
|
||||||
return <Link to={localLink}>{children}</Link>;
|
return <Link to={localLink}>{children}</Link>;
|
||||||
|
} else if (href) {
|
||||||
|
return (
|
||||||
|
<a href={href} {...props}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return <a {...props}>{children}</a>;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// we use a factory method, because react-markdown does not pass
|
// we use a factory method, because react-markdown does not pass
|
||||||
// base as prop down to our link component.
|
// base as prop down to our link component.
|
||||||
export const create = (base: string): FC<LinkProps> => {
|
export const create = (base?: string, protocolExtensions: ProtocolLinkRendererExtensionMap = {}): FC<LinkProps> => {
|
||||||
return props => <MarkdownLinkRenderer base={base} {...props} />;
|
return props => {
|
||||||
|
const protocolLinkContext = isLinkWithProtocol(props.href || "");
|
||||||
|
if (protocolLinkContext) {
|
||||||
|
const { link, protocol } = protocolLinkContext;
|
||||||
|
const ProtocolRenderer = protocolExtensions[protocol];
|
||||||
|
if (ProtocolRenderer) {
|
||||||
|
return (
|
||||||
|
<ProtocolRenderer protocol={protocol} href={link}>
|
||||||
|
{props.children}
|
||||||
|
</ProtocolRenderer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <MarkdownLinkRenderer base={base} {...props} />;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarkdownLinkRenderer;
|
export default MarkdownLinkRenderer;
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import Title from "../layout/Title";
|
|||||||
import { Subtitle } from "../layout";
|
import { Subtitle } from "../layout";
|
||||||
import { MemoryRouter } from "react-router-dom";
|
import { MemoryRouter } from "react-router-dom";
|
||||||
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
|
import { Binder, BinderContext } from "@scm-manager/ui-extensions";
|
||||||
|
import { ProtocolLinkRendererExtension, ProtocolLinkRendererProps } from "./markdownExtensions";
|
||||||
|
|
||||||
const Spacing = styled.div`
|
const Spacing = styled.div`
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
@@ -58,7 +59,30 @@ storiesOf("MarkdownView", module)
|
|||||||
<MarkdownView content={MarkdownInlineXml} />
|
<MarkdownView content={MarkdownInlineXml} />
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
.add("Links", () => <MarkdownView content={MarkdownLinks} basePath="/" />)
|
.add("Links", () => {
|
||||||
|
const binder = new Binder("custom protocol link renderer");
|
||||||
|
binder.bind("markdown-renderer.link.protocol", {
|
||||||
|
protocol: "scw",
|
||||||
|
renderer: ProtocolLinkRenderer
|
||||||
|
} as ProtocolLinkRendererExtension);
|
||||||
|
return (
|
||||||
|
<BinderContext.Provider value={binder}>
|
||||||
|
<MarkdownView content={MarkdownLinks} basePath="/scm/" />
|
||||||
|
</BinderContext.Provider>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.add("Links without Base Path", () => {
|
||||||
|
const binder = new Binder("custom protocol link renderer");
|
||||||
|
binder.bind("markdown-renderer.link.protocol", {
|
||||||
|
protocol: "scw",
|
||||||
|
renderer: ProtocolLinkRenderer
|
||||||
|
} as ProtocolLinkRendererExtension);
|
||||||
|
return (
|
||||||
|
<BinderContext.Provider value={binder}>
|
||||||
|
<MarkdownView content={MarkdownLinks} />
|
||||||
|
</BinderContext.Provider>
|
||||||
|
);
|
||||||
|
})
|
||||||
.add("Header Anchor Links", () => (
|
.add("Header Anchor Links", () => (
|
||||||
<MarkdownView
|
<MarkdownView
|
||||||
content={MarkdownChangelog}
|
content={MarkdownChangelog}
|
||||||
@@ -88,3 +112,14 @@ storiesOf("MarkdownView", module)
|
|||||||
);
|
);
|
||||||
})
|
})
|
||||||
.add("XSS Prevention", () => <MarkdownView content={MarkdownXss} skipHtml={false} />);
|
.add("XSS Prevention", () => <MarkdownView content={MarkdownXss} skipHtml={false} />);
|
||||||
|
|
||||||
|
export const ProtocolLinkRenderer: FC<ProtocolLinkRendererProps> = ({ protocol, href, children }) => {
|
||||||
|
return (
|
||||||
|
<div style={{ border: "1px dashed lightgray", padding: "2px" }}>
|
||||||
|
<h4>
|
||||||
|
Link: {href} [Protocol: {protocol}]
|
||||||
|
</h4>
|
||||||
|
<div>children: {children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import sanitize from "rehype-sanitize";
|
|||||||
import remark2rehype from "remark-rehype";
|
import remark2rehype from "remark-rehype";
|
||||||
import rehype2react from "rehype-react";
|
import rehype2react from "rehype-react";
|
||||||
import gfm from "remark-gfm";
|
import gfm from "remark-gfm";
|
||||||
import { binder } from "@scm-manager/ui-extensions";
|
import { BinderContext } from "@scm-manager/ui-extensions";
|
||||||
import ErrorBoundary from "../ErrorBoundary";
|
import ErrorBoundary from "../ErrorBoundary";
|
||||||
import { create as createMarkdownHeadingRenderer } from "./MarkdownHeadingRenderer";
|
import { create as createMarkdownHeadingRenderer } from "./MarkdownHeadingRenderer";
|
||||||
import { create as createMarkdownLinkRenderer } from "./MarkdownLinkRenderer";
|
import { create as createMarkdownLinkRenderer } from "./MarkdownLinkRenderer";
|
||||||
@@ -46,6 +46,7 @@ import raw from "rehype-raw";
|
|||||||
import slug from "rehype-slug";
|
import slug from "rehype-slug";
|
||||||
import merge from "deepmerge";
|
import merge from "deepmerge";
|
||||||
import { createComponentList } from "./createComponentList";
|
import { createComponentList } from "./createComponentList";
|
||||||
|
import { ProtocolLinkRendererExtension, ProtocolLinkRendererExtensionMap } from "./markdownExtensions";
|
||||||
|
|
||||||
type Props = RouteComponentProps &
|
type Props = RouteComponentProps &
|
||||||
WithTranslation & {
|
WithTranslation & {
|
||||||
@@ -94,6 +95,8 @@ const MarkdownErrorNotification: FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
class MarkdownView extends React.Component<Props, State> {
|
class MarkdownView extends React.Component<Props, State> {
|
||||||
|
static contextType = BinderContext;
|
||||||
|
|
||||||
static defaultProps: Partial<Props> = {
|
static defaultProps: Partial<Props> = {
|
||||||
enableAnchorHeadings: false,
|
enableAnchorHeadings: false,
|
||||||
skipHtml: false
|
skipHtml: false
|
||||||
@@ -143,7 +146,7 @@ class MarkdownView extends React.Component<Props, State> {
|
|||||||
mdastPlugins = []
|
mdastPlugins = []
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
const rendererFactory = binder.getExtension("markdown-renderer-factory");
|
const rendererFactory = this.context.getExtension("markdown-renderer-factory");
|
||||||
let remarkRendererList = renderers;
|
let remarkRendererList = renderers;
|
||||||
|
|
||||||
if (rendererFactory) {
|
if (rendererFactory) {
|
||||||
@@ -158,8 +161,19 @@ class MarkdownView extends React.Component<Props, State> {
|
|||||||
remarkRendererList.heading = createMarkdownHeadingRenderer(permalink);
|
remarkRendererList.heading = createMarkdownHeadingRenderer(permalink);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (basePath && !remarkRendererList.link) {
|
let protocolLinkRendererExtensions: ProtocolLinkRendererExtensionMap = {};
|
||||||
remarkRendererList.link = createMarkdownLinkRenderer(basePath);
|
if (!remarkRendererList.link) {
|
||||||
|
const extensionPoints = this.context.getExtensions(
|
||||||
|
"markdown-renderer.link.protocol"
|
||||||
|
) as ProtocolLinkRendererExtension[];
|
||||||
|
protocolLinkRendererExtensions = extensionPoints.reduce<ProtocolLinkRendererExtensionMap>(
|
||||||
|
(prev, { protocol, renderer }) => {
|
||||||
|
prev[protocol] = renderer;
|
||||||
|
return prev;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
remarkRendererList.link = createMarkdownLinkRenderer(basePath, protocolLinkRendererExtensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!remarkRendererList.code) {
|
if (!remarkRendererList.code) {
|
||||||
@@ -188,7 +202,10 @@ class MarkdownView extends React.Component<Props, State> {
|
|||||||
attributes: {
|
attributes: {
|
||||||
code: ["className"] // Allow className for code elements, this is necessary to extract the code language
|
code: ["className"] // Allow className for code elements, this is necessary to extract the code language
|
||||||
},
|
},
|
||||||
clobberPrefix: "" // Do not prefix user-provided ids and class names
|
clobberPrefix: "", // Do not prefix user-provided ids and class names,
|
||||||
|
protocols: {
|
||||||
|
href: Object.keys(protocolLinkRendererExtensions)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.use(rehype2react, {
|
.use(rehype2react, {
|
||||||
|
|||||||
15
scm-ui/ui-components/src/markdown/markdownExtensions.ts
Normal file
15
scm-ui/ui-components/src/markdown/markdownExtensions.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
export type ProtocolLinkRendererProps = {
|
||||||
|
protocol: string;
|
||||||
|
href: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProtocolLinkRendererExtension = {
|
||||||
|
protocol: string;
|
||||||
|
renderer: FC<ProtocolLinkRendererProps>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ProtocolLinkRendererExtensionMap = {
|
||||||
|
[protocol: string]: FC<ProtocolLinkRendererProps>;
|
||||||
|
}
|
||||||
@@ -48,11 +48,8 @@ export const createRemark2RehypeCodeRendererAdapter = (remarkRenderer: any) => {
|
|||||||
|
|
||||||
export const createRemark2RehypeLinkRendererAdapter = (remarkRenderer: any) => {
|
export const createRemark2RehypeLinkRendererAdapter = (remarkRenderer: any) => {
|
||||||
return ({ node, children }: any) => {
|
return ({ node, children }: any) => {
|
||||||
const renderProps = {
|
|
||||||
href: node.properties.href || ""
|
|
||||||
};
|
|
||||||
children = children || [];
|
children = children || [];
|
||||||
return React.createElement(remarkRenderer, renderProps, ...children);
|
return React.createElement(remarkRenderer, node.properties, ...children);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user