Create api for markdown ast plugins (#1578)

Currently we only have the option to adjust rendering of the markdown output, but no option to change the generated ast tree before rendering takes place (i.e. to adjust the structure like when replacing text with links). This PR adds a new api to create ast plugins that can be integrated with the markdown view component. This is intended to be backwards-compatible and work independently from the underlying implementation.

Co-authored-by: Sebastian Sdorra <sebastian.sdorra@cloudogu.com>
This commit is contained in:
Konstantin Schaper
2021-03-11 10:28:18 +01:00
committed by GitHub
parent 0d3339b0cb
commit e66553705f
17 changed files with 69503 additions and 147320 deletions

View File

@@ -0,0 +1,2 @@
- type: added
description: create api for markdown ast plugins ([#1578](https://github.com/scm-manager/scm-manager/pull/1578))

View File

@@ -39,9 +39,10 @@ const queryClient = new QueryClient({
type Props = LegacyContext & {
index?: IndexResources;
me?: Me;
devtools?: boolean;
};
const ApiProvider: FC<Props> = ({ children, index, me, onMeFetched, onIndexFetched }) => {
const ApiProvider: FC<Props> = ({ children, index, me, onMeFetched, onIndexFetched, devtools = true }) => {
useEffect(() => {
if (index) {
queryClient.setQueryData("index", index);
@@ -63,7 +64,7 @@ const ApiProvider: FC<Props> = ({ children, index, me, onMeFetched, onIndexFetch
<LegacyContextProvider onIndexFetched={onIndexFetched} onMeFetched={onMeFetched}>
{children}
</LegacyContextProvider>
<ReactQueryDevtools initialIsOpen={false} />
{devtools ? <ReactQueryDevtools initialIsOpen={false} /> : null}
</QueryClientProvider>
);
};

View File

@@ -39,6 +39,7 @@ const withApiProvider = (storyFn) => {
groups: [],
_links: {}
},
devtools: false,
children: storyFn()
});
}

View File

@@ -42,6 +42,7 @@
"@types/refractor": "^3.0.0",
"@types/styled-components": "^5.1.0",
"@types/to-camel-case": "^1.0.0",
"@types/unist": "^2.0.3",
"css": "^3.0.0",
"enzyme-context": "^1.1.2",
"enzyme-context-react-router-4": "^2.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -67,7 +67,7 @@ export { default as GroupAutocomplete } from "./GroupAutocomplete";
export { default as UserAutocomplete } from "./UserAutocomplete";
export { default as BranchSelector } from "./BranchSelector";
export { default as Breadcrumb } from "./Breadcrumb";
export { default as MarkdownView } from "./MarkdownView";
export { default as MarkdownView } from "./markdown/MarkdownView";
export { default as SyntaxHighlighter } from "./SyntaxHighlighter";
export { default as ErrorBoundary } from "./ErrorBoundary";
export { default as OverviewPageActions } from "./OverviewPageActions";
@@ -77,7 +77,8 @@ export { default as CardColumn } from "./CardColumn";
export { default as CardColumnSmall } from "./CardColumnSmall";
export { default as CommaSeparatedList } from "./CommaSeparatedList";
export { default as SplitAndReplace, Replacement } from "./SplitAndReplace";
export { regExpPattern as changesetShortLinkRegex } from "./remarkChangesetShortLinkParser";
export { regExpPattern as changesetShortLinkRegex } from "./markdown/remarkChangesetShortLinkParser";
export * from "./markdown/PluginApi";
export { default as comparators } from "./comparators";

View File

@@ -23,7 +23,7 @@
*/
import React, { FC } from "react";
import SyntaxHighlighter from "./SyntaxHighlighter";
import SyntaxHighlighter from "../SyntaxHighlighter";
import { ExtensionPoint, useBinder } from "@scm-manager/ui-extensions";
import { useIndexLinks } from "@scm-manager/ui-api";

View File

@@ -25,10 +25,10 @@ import React, { FC, ReactNode, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { urls } from "@scm-manager/ui-api";
import styled from "styled-components";
import Icon from "./Icon";
import Tooltip from "./Tooltip";
import Icon from "../Icon";
import Tooltip from "../Tooltip";
import { useTranslation } from "react-i18next";
import copyToClipboard from "./CopyToClipboard";
import copyToClipboard from "../CopyToClipboard";
/**
* Adds anchor links to markdown headings.

View File

@@ -23,7 +23,7 @@
*/
import React, { FC } from "react";
import { Link, useLocation } from "react-router-dom";
import ExternalLink from "./navigation/ExternalLink";
import ExternalLink from "../navigation/ExternalLink";
import { urls } from "@scm-manager/ui-api";
const externalLinkRegex = new RegExp("^http(s)?://");

View File

@@ -26,16 +26,16 @@ import { storiesOf } from "@storybook/react";
import MarkdownView from "./MarkdownView";
import styled from "styled-components";
import TestPage from "./__resources__/test-page.md";
import MarkdownWithoutLang from "./__resources__/markdown-without-lang.md";
import MarkdownXmlCodeBlock from "./__resources__/markdown-xml-codeblock.md";
import MarkdownUmlCodeBlock from "./__resources__/markdown-uml-codeblock.md";
import MarkdownInlineXml from "./__resources__/markdown-inline-xml.md";
import MarkdownLinks from "./__resources__/markdown-links.md";
import MarkdownCommitLinks from "./__resources__/markdown-commit-link.md";
import MarkdownXss from "./__resources__/markdown-xss.md";
import Title from "./layout/Title";
import { Subtitle } from "./layout";
import TestPage from "../__resources__/test-page.md";
import MarkdownWithoutLang from "../__resources__/markdown-without-lang.md";
import MarkdownXmlCodeBlock from "../__resources__/markdown-xml-codeblock.md";
import MarkdownUmlCodeBlock from "../__resources__/markdown-uml-codeblock.md";
import MarkdownInlineXml from "../__resources__/markdown-inline-xml.md";
import MarkdownLinks from "../__resources__/markdown-links.md";
import MarkdownCommitLinks from "../__resources__/markdown-commit-link.md";
import MarkdownXss from "../__resources__/markdown-xss.md";
import Title from "../layout/Title";
import { Subtitle } from "../layout";
import { MemoryRouter } from "react-router-dom";
import { Binder, BinderContext } from "@scm-manager/ui-extensions";

View File

@@ -27,13 +27,15 @@ import Markdown from "react-markdown";
import MarkdownWithHtml from "react-markdown/with-html";
import gfm from "remark-gfm";
import { binder } from "@scm-manager/ui-extensions";
import ErrorBoundary from "./ErrorBoundary";
import ErrorBoundary from "../ErrorBoundary";
import { create as createMarkdownHeadingRenderer } from "./MarkdownHeadingRenderer";
import { create as createMarkdownLinkRenderer } from "./MarkdownLinkRenderer";
import { useTranslation, WithTranslation, withTranslation } from "react-i18next";
import Notification from "./Notification";
import Notification from "../Notification";
import { createTransformer } from "./remarkChangesetShortLinkParser";
import MarkdownCodeRenderer from "./MarkdownCodeRenderer";
import { AstPlugin } from "./PluginApi";
import createMdastPlugin from "./createMdastPlugin";
type Props = RouteComponentProps &
WithTranslation & {
@@ -45,6 +47,7 @@ type Props = RouteComponentProps &
// basePath for markdown links
basePath?: string;
permalink?: string;
mdastPlugins?: AstPlugin[];
};
type State = {
@@ -117,7 +120,17 @@ class MarkdownView extends React.Component<Props, State> {
}
render() {
const { content, renderers, renderContext, enableAnchorHeadings, skipHtml, basePath, permalink, t } = this.props;
const {
content,
renderers,
renderContext,
enableAnchorHeadings,
skipHtml,
basePath,
permalink,
t,
mdastPlugins = []
} = this.props;
const rendererFactory = binder.getExtension("markdown-renderer-factory");
let rendererList = renderers;
@@ -142,11 +155,13 @@ class MarkdownView extends React.Component<Props, State> {
rendererList.code = MarkdownCodeRenderer;
}
const plugins = [...mdastPlugins, createTransformer(t)].map(createMdastPlugin);
const baseProps = {
className: "content is-word-break",
renderers: rendererList,
plugins: [gfm],
astPlugins: [createTransformer(t)],
astPlugins: plugins,
children: content
};

View File

@@ -0,0 +1,9 @@
import { Node, Parent } from "unist";
export type Visitor = (node: Node, index: number, parent?: Parent) => void;
export type AstPluginContext = {
visit: (type: string, visitor: Visitor) => void;
};
export type AstPlugin = (context: AstPluginContext) => void;

View File

@@ -0,0 +1,12 @@
import { AstPlugin } from "./PluginApi";
// @ts-ignore No types available
import visit from "unist-util-visit";
export default function createMdastPlugin(plugin: AstPlugin): any {
return (tree: any) => {
plugin({
visit: (type, visitor) => visit(tree, type, visitor)
});
return tree;
};
}

View File

@@ -22,10 +22,10 @@
* SOFTWARE.
*/
import { nameRegex } from "./validation";
// @ts-ignore No types available
import visit from "unist-util-visit";
import { nameRegex } from "../validation";
import { TFunction } from "i18next";
import { AstPlugin } from "./PluginApi";
import { Node, Parent } from "unist";
const namePartRegex = nameRegex.source.substring(1, nameRegex.source.length - 1);
@@ -42,14 +42,14 @@ function match(value: string): RegExpMatchArray[] {
return matches;
}
export const createTransformer = (t: TFunction) => {
return (tree: any) => {
visit(tree, "text", (node: any, index: number, parent: any) => {
if (parent.type === "link" || !node.value) {
export const createTransformer = (t: TFunction): AstPlugin => {
return ({ visit }) => {
visit("text", (node: Node, index: number, parent?: Parent) => {
if (!parent || parent.type === "link" || !node.value) {
return;
}
let nodeText = node.value;
let nodeText = node.value as string;
const matches = match(nodeText);
if (matches.length > 0) {
@@ -89,12 +89,11 @@ export const createTransformer = (t: TFunction) => {
});
}
parent.children![index] = {
parent.children[index] = {
type: "text",
children
};
}
});
return tree;
};
};