Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 22 additions & 3 deletions bricks/markdown/docs/eo-markdown-display.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,41 @@ properties:
- carrot
- broccoli

A [link](http://example.com).
An [external link](http://example.com).

An [internal link](/playground).

![Image](https://upload.wikimedia.org/wikipedia/commons/5/5c/Icon-pictures.png "icon")

An image in a link:

[![Image](https://upload.wikimedia.org/wikipedia/commons/5/5c/Icon-pictures.png "icon")](http://example.com)

> Markdown uses email-style
characters for blockquoting.
>
> Multiple paragraphs need to be prepended individually.

Most inline <abbr title="Hypertext Markup Language">HTML</abbr> tags are supported.

Here is a `javascript` code below:

```js
function test() {
alert("Hello");
}
```

| Name | Gender | Age |
| - | - | -: |
| Jim | Male | 16 |
| Lucy | Female | 17 |

Most inline <abbr title="Hypertext Markup Language">HTML</abbr> tags are supported.

Note: inline HTML are sanitized, and attributes like `class` and `style` are removed, see [`defaultScheme`](https://github.com/syntax-tree/hast-util-sanitize?tab=readme-ov-file#defaultschema) of hast-util-sanitize.

<em class="em" title="em" style="color:blue">em</em>

<img src="https://upload.wikimedia.org/wikipedia/commons/3/3f/Fronalpstock_big.jpg" alt="pictures">

<img src="/404" onerror="alert('oops')">
````
11 changes: 0 additions & 11 deletions bricks/markdown/src/markdown-display/host-context.css

This file was deleted.

14 changes: 11 additions & 3 deletions bricks/markdown/src/markdown-display/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import React from "react";
import { createDecorators } from "@next-core/element";
import { ReactNextElement } from "@next-core/react-element";
import "@next-core/theme";
import { MarkdownComponent } from "@next-shared/markdown";
import { MarkdownComponent, type LinkOptions } from "@next-shared/markdown";
import "@next-shared/markdown/dist/esm/host-context.css";
import styleText from "./styles.shadow.css";
import "./host-context.css";

const { defineElement, property } = createDecorators();

Expand All @@ -23,7 +23,15 @@ class MarkdownDisplay extends ReactNextElement implements MarkdownDisplayProps {
@property()
accessor content: string | undefined;

@property({ attribute: false })
accessor linkOptions: LinkOptions | undefined;

render() {
return <MarkdownComponent content={this.content} />;
return (
<MarkdownComponent
content={this.content}
linkOptions={this.linkOptions}
/>
);
}
}
27 changes: 1 addition & 26 deletions bricks/markdown/src/markdown-display/styles.shadow.css
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,4 @@
}

@import "~@next-shared/markdown/dist/esm/prism-tomorrow.css";

pre {
white-space: pre-wrap;
background-color: var(--palette-gray-3);
border-radius: 6px;
padding: 1em;
}

:not(pre) > code {
color: var(--eo-markdown-display-code-color);
background: var(--eo-markdown-display-code-background);
margin: 0 2px;
padding: 1px 6px;
white-space: nowrap;
border-radius: 3px;
}

blockquote {
border-left: 6px solid var(--eo-markdown-display-blockquote-border-color);
padding: 0 1em;
}

:not(blockquote) > blockquote {
margin-left: 0;
margin-right: 0;
}
@import "~@next-shared/markdown/dist/esm/default.css";
5 changes: 5 additions & 0 deletions shared/markdown/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,16 @@
"build:types": "tsc --emitDeclarationOnly --declaration --declarationDir dist/types --project tsconfig.build.json"
},
"dependencies": {
"@next-core/theme": "^1.5.4",
"hast-util-to-string": "^3.0.0",
"is-absolute-url": "^4.0.1",
"prismjs": "^1.29.0",
"react": "0.0.0-experimental-ee8509801-20230117",
"refractor": "^4.8.1",
"rehype-react": "^8.0.0",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.0",
"remark-parse": "^11.0.0",
"remark-rehype": "^11.1.0",
"unified": "^11.0.4",
Expand Down
22 changes: 18 additions & 4 deletions shared/markdown/src/MarkdownComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,40 @@ import { useEffect, useState } from "react";
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkGfm from "remark-gfm";
import remarkToRehype from "remark-rehype";
import rehypeRaw from "rehype-raw";
import rehypeSanitize from "rehype-sanitize";
import rehypeReact, { Options as RehypeReactOptions } from "rehype-react";
import { rehypePrism } from "./rehypePrism.js";
import { rehypeLinks, type LinkOptions } from "./rehypeLinks.js";

const production = { Fragment, jsx, jsxs };

export interface MarkdownComponentProps {
content?: string;
linkOptions?: LinkOptions;
}

export type { LinkOptions };

// Reference https://github.com/remarkjs/react-remark/blob/39553e5f5c9e9b903bebf261788ff45130668de0/src/index.ts
export function MarkdownComponent({ content }: MarkdownComponentProps) {
export function MarkdownComponent({
content,
linkOptions,
}: MarkdownComponentProps) {
const [reactContent, setReactContent] = useState<JSX.Element | null>(null);

useEffect(() => {
let ignore = false;
unified()
.use(remarkParse)
.use(remarkToRehype)
.use([rehypePrism])
.use(remarkGfm)
.use(remarkToRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize)
.use(rehypePrism)
.use(rehypeLinks, linkOptions)
.use(rehypeReact, production as RehypeReactOptions)
.process(content)
.then((vFile) => {
Expand All @@ -39,7 +53,7 @@ export function MarkdownComponent({ content }: MarkdownComponentProps) {
return () => {
ignore = true;
};
}, [content]);
}, [content, linkOptions]);

return reactContent;
}
48 changes: 48 additions & 0 deletions shared/markdown/src/default.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
pre {
white-space: pre-wrap;
background-color: var(--palette-gray-3);
border-radius: 6px;
padding: 1em;
}

:not(pre) > code {
color: var(--shared-markdown-code-color);
background: var(--shared-markdown-code-background);
margin: 0 2px;
padding: 1px 6px;
white-space: nowrap;
border-radius: 3px;
}

blockquote {
border-left: 6px solid #bcc0c5;
padding: 0 1em;
}

:not(blockquote) > blockquote {
margin-left: 0;
margin-right: 0;
}

img {
max-width: 100%;
}

table {
border-collapse: collapse;
margin: 1em 0;
}

th,
td {
border: 1px solid var(--shared-markdown-table-border-color);
padding: 6px 13px;
}

tr {
background-color: var(--shared-markdown-tr-background);
}

tr:nth-child(2n) {
background-color: var(--shared-markdown-tr-background-alt);
}
16 changes: 16 additions & 0 deletions shared/markdown/src/host-context.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
:root {
--shared-markdown-code-color: #b30056;
--shared-markdown-code-background: #ffe6ec;
--shared-markdown-table-border-color: #ccc;
--shared-markdown-tr-background: #fff;
--shared-markdown-tr-background-alt: #f8f8f8;
}

html[data-theme="dark"],
html[data-theme="dark-v2"] {
--shared-markdown-code-color: #f3679a;
--shared-markdown-code-background: var(--color-fill-bg-base-1);
--shared-markdown-table-border-color: var(--color-border-divider-line);
--shared-markdown-tr-background: rgba(255, 255, 255, 0);
--shared-markdown-tr-background-alt: var(--color-fill-bg-base-1);
}
46 changes: 46 additions & 0 deletions shared/markdown/src/rehypeLinks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { visit } from "unist-util-visit";
import type { Element } from "hast";
import isAbsoluteUrl from "is-absolute-url";

export interface LinkOptions {
blankTarget?: {
policy?: "never" | "always" | "external-only" | "absolute-only";
};
}

export function rehypeLinks(options?: LinkOptions) {
// Ref https://github.com/rehypejs/rehype-external-links
function visitor(node: Element) {
const blankTargetPolicy = options?.blankTarget?.policy ?? "external-only";
if (blankTargetPolicy === "never") {
return;
}
const href = node.properties.href;
if (node.tagName === "a" && typeof href === "string") {
let shouldUseBlankTarget = false;

switch (blankTargetPolicy) {
case "always":
shouldUseBlankTarget = true;
break;
case "absolute-only":
shouldUseBlankTarget = isAbsoluteUrl(href) || href.startsWith("//");
break;
default: {
const url = new URL(href, location.origin).toString();
shouldUseBlankTarget = !url.startsWith(`${location.origin}/`);
break;
}
}

if (shouldUseBlankTarget) {
node.properties.target = "_blank";
node.properties.rel = "noopener noreferrer nofollow";
}
}
}

return (tree: Element) => {
visit(tree, "element", visitor);
};
}
Loading