mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-07 22:15:45 +01:00
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:
committed by
GitHub
parent
0d3339b0cb
commit
e66553705f
2
gradle/changelog/markdown-ast-plugins.yaml
Normal file
2
gradle/changelog/markdown-ast-plugins.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
- type: added
|
||||
description: create api for markdown ast plugins ([#1578](https://github.com/scm-manager/scm-manager/pull/1578))
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,6 +39,7 @@ const withApiProvider = (storyFn) => {
|
||||
groups: [],
|
||||
_links: {}
|
||||
},
|
||||
devtools: false,
|
||||
children: storyFn()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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.
|
||||
@@ -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)?://");
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
9
scm-ui/ui-components/src/markdown/PluginApi.ts
Normal file
9
scm-ui/ui-components/src/markdown/PluginApi.ts
Normal 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;
|
||||
12
scm-ui/ui-components/src/markdown/createMdastPlugin.ts
Normal file
12
scm-ui/ui-components/src/markdown/createMdastPlugin.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user