Add an option to enable header links for markdown (#6831)

This commit is contained in:
pngwn 2023-12-18 20:06:20 +00:00 committed by GitHub
parent 1401d99ade
commit f3abde8088
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 148 additions and 22 deletions

View File

@ -0,0 +1,7 @@
---
"@gradio/app": minor
"@gradio/markdown": minor
"gradio": minor
---
feat:Add an option to enable header links for markdown

File diff suppressed because one or more lines are too long

View File

@ -218,6 +218,6 @@ MIT
"""
with gr.Blocks(css=css) as demo:
gr.Markdown(value=md)
gr.Markdown(value=md, header_links=True)
demo.launch()

View File

@ -41,6 +41,7 @@ class Markdown(Component):
render: bool = True,
sanitize_html: bool = True,
line_breaks: bool = False,
header_links: bool = False,
):
"""
Parameters:
@ -56,6 +57,7 @@ class Markdown(Component):
render: If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.
sanitize_html: If False, will disable HTML sanitization when converted from markdown. This is not recommended, as it can lead to security vulnerabilities.
line_breaks: If True, will enable Github-flavored Markdown line breaks in chatbot messages. If False (default), single new lines will be ignored.
header_links: If True, will automatically create anchors for headings, displaying a link icon on hover.
"""
self.rtl = rtl
if latex_delimiters is None:
@ -63,6 +65,7 @@ class Markdown(Component):
self.latex_delimiters = latex_delimiters
self.sanitize_html = sanitize_html
self.line_breaks = line_breaks
self.header_links = header_links
super().__init__(
label=label,

View File

@ -81,7 +81,7 @@
.app {
position: relative;
margin: auto;
padding: var(--size-4);
padding: var(--size-4) var(--size-8);
width: 100%;
height: 100%;
}

View File

@ -29,11 +29,18 @@
right: string;
display: boolean;
}[];
export let header_links = false;
$: label, gradio.dispatch("change");
</script>
<Block {visible} {elem_id} {elem_classes} container={false}>
<Block
{visible}
{elem_id}
{elem_classes}
container={false}
allow_overflow={true}
>
<StatusTracker
autoscroll={gradio.autoscroll}
i18n={gradio.i18n}
@ -51,6 +58,7 @@
{latex_delimiters}
{sanitize_html}
{line_breaks}
{header_links}
/>
</div>
</Block>

View File

@ -21,8 +21,10 @@
"@types/katex": "^0.16.0",
"@types/prismjs": "1.26.3",
"dompurify": "^3.0.3",
"github-slugger": "^2.0.0",
"katex": "^0.16.7",
"marked": "^11.0.0",
"marked-gfm-heading-id": "^3.1.2",
"marked-highlight": "^2.0.1",
"prismjs": "1.29.0"
}

View File

@ -16,6 +16,7 @@
right: string;
display: boolean;
}[];
export let header_links = false;
const dispatch = createEventDispatcher<{ change: undefined }>();
@ -36,6 +37,7 @@
{sanitize_html}
{line_breaks}
chatbot={false}
{header_links}
/>
</div>
@ -55,7 +57,6 @@
div {
max-width: 100%;
overflow-x: auto;
}
.min {

View File

@ -3,7 +3,8 @@
import DOMPurify from "dompurify";
import render_math_in_element from "katex/dist/contrib/auto-render.js";
import "katex/dist/katex.min.css";
import { marked } from "./utils";
import { create_marked } from "./utils";
import "./prism.css";
export let chatbot = true;
@ -16,15 +17,25 @@
}[] = [];
export let render_markdown = true;
export let line_breaks = true;
export let header_links = false;
let el: HTMLSpanElement;
let html: string;
marked.use({ breaks: line_breaks });
const marked = create_marked({
header_links,
line_breaks
});
const is_external_url = (link: string | null): boolean =>
!!(link && new URL(link, location.href).origin !== location.origin);
DOMPurify.addHook("afterSanitizeAttributes", function (node) {
if ("target" in node) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
if (is_external_url(node.getAttribute("href"))) {
node.setAttribute("target", "_blank");
node.setAttribute("rel", "noopener noreferrer");
}
}
});
@ -122,4 +133,36 @@
span :global(p:not(:first-child)) {
margin-top: var(--spacing-xxl);
}
span :global(.md-header-anchor) {
/* position: absolute; */
margin-left: -25px;
padding-right: 8px;
line-height: 1;
color: var(--body-text-color-subdued);
opacity: 0;
}
span :global(h1:hover .md-header-anchor),
span :global(h2:hover .md-header-anchor),
span :global(h3:hover .md-header-anchor),
span :global(h4:hover .md-header-anchor),
span :global(h5:hover .md-header-anchor),
span :global(h6:hover .md-header-anchor) {
opacity: 1;
}
span.md :global(.md-header-anchor > svg) {
color: var(--body-text-color-subdued);
}
span :global(h1),
span :global(h2),
span :global(h3),
span :global(h4),
span :global(h5),
span :global(h6) {
display: flex;
align-items: center;
}
</style>

View File

@ -1,13 +1,18 @@
import { marked, type Renderer } from "marked";
import { markedHighlight } from "marked-highlight";
import { gfmHeadingId } from "marked-gfm-heading-id";
import Prism from "prismjs";
import "prismjs/components/prism-python";
import "prismjs/components/prism-latex";
import "prismjs/components/prism-bash";
import GithubSlugger from "github-slugger";
// import loadLanguages from "prismjs/components/";
// loadLanguages(["python", "latex"]);
const LINK_ICON_CODE = `<svg class="md-link-icon" viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true" fill="currentColor"><path d="m7.775 3.275 1.25-1.25a3.5 3.5 0 1 1 4.95 4.95l-2.5 2.5a3.5 3.5 0 0 1-4.95 0 .751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018 1.998 1.998 0 0 0 2.83 0l2.5-2.5a2.002 2.002 0 0 0-2.83-2.83l-1.25 1.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042Zm-4.69 9.64a1.998 1.998 0 0 0 2.83 0l1.25-1.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042l-1.25 1.25a3.5 3.5 0 1 1-4.95-4.95l2.5-2.5a3.5 3.5 0 0 1 4.95 0 .751.751 0 0 1-.018 1.042.751.751 0 0 1-1.042.018 1.998 1.998 0 0 0-2.83 0l-2.5 2.5a1.998 1.998 0 0 0 0 2.83Z"></path></svg>`;
const COPY_ICON_CODE = `<svg
xmlns="http://www.w3.org/2000/svg"
width="100%"
@ -96,21 +101,59 @@ const renderer: Partial<Omit<Renderer, "constructor" | "options">> = {
}
};
marked.use(
{
gfm: true,
pedantic: false
},
markedHighlight({
highlight: (code: string, lang: string) => {
if (Prism.languages[lang]) {
return Prism.highlight(code, Prism.languages[lang], lang);
const slugger = new GithubSlugger();
export function create_marked({
header_links,
line_breaks
}: {
header_links: boolean;
line_breaks: boolean;
}): typeof marked {
marked.use(
{
gfm: true,
pedantic: false,
breaks: line_breaks
},
markedHighlight({
highlight: (code: string, lang: string) => {
if (Prism.languages[lang]) {
return Prism.highlight(code, Prism.languages[lang], lang);
}
return code;
}
return code;
}),
{ renderer }
);
if (header_links) {
if (header_links) {
marked.use(gfmHeadingId());
marked.use({
extensions: [
{
name: "heading",
level: "block",
renderer(token) {
const raw = token.raw
.toLowerCase()
.trim()
.replace(/<[!\/a-z].*?>/gi, "");
const id = "h" + slugger.slug(raw);
const level = token.depth;
const text = this.parser.parseInline(token.tokens!);
return `<h${level} id="${id}"><a class="md-header-anchor" href="#${id}">${LINK_ICON_CODE}</a>${text}</h${level}>\n`;
}
}
]
});
}
}),
{ renderer }
);
}
return marked;
}
export function copy(node: HTMLDivElement): any {
node.addEventListener("click", handle_copy);

19
pnpm-lock.yaml generated
View File

@ -1104,12 +1104,18 @@ importers:
dompurify:
specifier: ^3.0.3
version: 3.0.3
github-slugger:
specifier: ^2.0.0
version: 2.0.0
katex:
specifier: ^0.16.7
version: 0.16.7
marked:
specifier: ^11.0.0
version: 11.0.0
marked-gfm-heading-id:
specifier: ^3.1.2
version: 3.1.2(marked@11.0.0)
marked-highlight:
specifier: ^2.0.1
version: 2.0.1(marked@11.0.0)
@ -10675,6 +10681,10 @@ packages:
resolution: {integrity: sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==}
dev: true
/github-slugger@2.0.0:
resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==}
dev: false
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -12078,6 +12088,15 @@ packages:
react: 18.2.0
dev: true
/marked-gfm-heading-id@3.1.2(marked@11.0.0):
resolution: {integrity: sha512-SdIZvhNxDgndFkDa2WRcFP4ahYm6k6hoHdTCN+fD7HRiI/R3Eimcw/Yl7ikQ+0KUuDpi75NnYQiThZnZsNr9Dg==}
peerDependencies:
marked: '>=4 <12'
dependencies:
github-slugger: 2.0.0
marked: 11.0.0
dev: false
/marked-highlight@2.0.1(marked@11.0.0):
resolution: {integrity: sha512-LDUfR/zDvD+dJ+lQOWHkxvBLNxiXcaN8pBtwJ/i4pI0bkDC/Ef6Mz1qUrAuHXfnpdr2rabdMpVFhqFuU+5Mskg==}
peerDependencies: