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:
Konstantin Schaper
2021-04-29 13:15:22 +02:00
committed by GitHub
parent 8f91c217fc
commit 32b268e6f5
11 changed files with 339 additions and 65 deletions

View File

@@ -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

View 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))

View File

@@ -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)

View File

@@ -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>

View File

@@ -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,

View 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();
}); });
}); });

View File

@@ -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;

View File

@@ -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>
);
};

View File

@@ -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, {

View 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>;
}

View File

@@ -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);
}; };
}; };