Skip to content

Commit bc2b0d6

Browse files
authored
feat: BROS-587: Support HTML in Markdown (#8747)
Co-authored-by: hlomzik <hlomzik@users.noreply.github.com>
1 parent a9f7a1a commit bc2b0d6

File tree

4 files changed

+192
-86
lines changed

4 files changed

+192
-86
lines changed
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import ReactMarkdown from "react-markdown";
2+
import type { Components } from "react-markdown";
3+
import rehypeRaw from "rehype-raw";
4+
5+
// Custom markdown components with Tailwind styling
6+
const markdownComponents: Components = {
7+
// Headings
8+
h1: (props) => <h1 className="text-display-small font-bold mb-wide mt-wider" {...props} />,
9+
h2: (props) => <h2 className="text-headline-large font-bold mb-base mt-wide" {...props} />,
10+
h3: (props) => <h3 className="text-headline-medium font-semibold mb-base mt-wide" {...props} />,
11+
h4: (props) => <h4 className="text-headline-small font-semibold mb-tight mt-base" {...props} />,
12+
h5: (props) => <h5 className="text-title-large font-semibold mb-tight mt-base" {...props} />,
13+
h6: (props) => <h6 className="text-title-medium font-semibold mb-tight mt-base" {...props} />,
14+
15+
// Paragraphs
16+
p: (props) => <p className="text-body-medium mb-base leading-body-medium" {...props} />,
17+
18+
// Lists
19+
ul: (props) => <ul className="list-disc pl-base mb-base space-y-tighter" {...props} />,
20+
ol: (props) => <ol className="list-decimal pl-base mb-base space-y-tighter" {...props} />,
21+
li: (props) => <li className="text-body-medium pl-tight ml-base" {...props} />,
22+
23+
// Code
24+
code: ({ inline, ...props }: { inline?: boolean } & React.HTMLAttributes<HTMLElement>) => {
25+
if (inline) {
26+
return (
27+
<code
28+
className="bg-neutral-emphasis text-neutral-content px-tighter py-tightest rounded-smallest font-mono text-body-smaller"
29+
{...props}
30+
/>
31+
);
32+
}
33+
return <code className="font-mono text-body-small" {...props} />;
34+
},
35+
pre: (props) => (
36+
<pre
37+
className="bg-neutral-surface-inset border border-neutral-border rounded-small p-base mb-base overflow-x-auto"
38+
{...props}
39+
/>
40+
),
41+
42+
// Blockquotes
43+
blockquote: (props) => (
44+
<blockquote
45+
className="border-l-4 border-primary-border pl-base ml-base mb-base italic text-neutral-content-subtle"
46+
{...props}
47+
/>
48+
),
49+
50+
// Links
51+
a: (props) => <a className="text-primary-content hover:text-primary-content-hover underline" {...props} />,
52+
53+
// Horizontal rule
54+
hr: (props) => <hr className="border-neutral-border my-wide" {...props} />,
55+
56+
// Tables
57+
table: (props) => (
58+
<div className="overflow-x-auto mb-base">
59+
<table className="min-w-full border-collapse border border-neutral-border" {...props} />
60+
</div>
61+
),
62+
thead: (props) => <thead className="bg-neutral-surface" {...props} />,
63+
tbody: (props) => <tbody {...props} />,
64+
tr: (props) => <tr className="border-b border-neutral-border" {...props} />,
65+
th: (props) => (
66+
<th className="border border-neutral-border px-base py-tight text-left font-semibold text-body-medium" {...props} />
67+
),
68+
td: (props) => <td className="border border-neutral-border px-base py-tight text-body-medium" {...props} />,
69+
70+
// Strong and emphasis
71+
strong: (props) => <strong className="font-bold" {...props} />,
72+
em: (props) => <em className="italic" {...props} />,
73+
74+
// Strikethrough
75+
del: (props) => <del className="line-through text-neutral-content-subtle" {...props} />,
76+
};
77+
78+
interface MarkdownProps {
79+
text: string;
80+
allowHtml?: boolean;
81+
}
82+
83+
export const Markdown = ({ text, allowHtml = false }: MarkdownProps) => {
84+
return (
85+
<ReactMarkdown rehypePlugins={allowHtml ? [rehypeRaw] : undefined} components={markdownComponents}>
86+
{text}
87+
</ReactMarkdown>
88+
);
89+
};

web/libs/editor/src/tags/visual/Markdown.jsx

Lines changed: 2 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { observer } from "mobx-react";
22
import { types } from "mobx-state-tree";
3-
import ReactMarkdown from "react-markdown";
43

4+
import { Markdown } from "../../components/Markdown/Markdown";
55
import Registry from "../../core/Registry";
66
import Tree from "../../core/Tree";
77
import ProcessAttrsMixin from "../../mixins/ProcessAttrs";
@@ -10,86 +10,6 @@ import { AnnotationMixin } from "../../mixins/AnnotationMixin";
1010
import { parseValue } from "../../utils/data";
1111
import { guidGenerator } from "../../utils/unique";
1212

13-
// Custom markdown components with Tailwind styling
14-
const markdownComponents = {
15-
// Headings
16-
h1: ({ children }) => <h1 className="text-display-small font-bold mb-wide mt-wider">{children}</h1>,
17-
h2: ({ children }) => <h2 className="text-headline-large font-bold mb-base mt-wide">{children}</h2>,
18-
h3: ({ children }) => <h3 className="text-headline-medium font-semibold mb-base mt-wide">{children}</h3>,
19-
h4: ({ children }) => <h4 className="text-headline-small font-semibold mb-tight mt-base">{children}</h4>,
20-
h5: ({ children }) => <h5 className="text-title-large font-semibold mb-tight mt-base">{children}</h5>,
21-
h6: ({ children }) => <h6 className="text-title-medium font-semibold mb-tight mt-base">{children}</h6>,
22-
23-
// Paragraphs
24-
p: ({ children }) => <p className="text-body-medium mb-base leading-body-medium">{children}</p>,
25-
26-
// Lists
27-
ul: ({ children }) => <ul className="list-disc pl-base mb-base space-y-tighter">{children}</ul>,
28-
ol: ({ children }) => <ol className="list-decimal pl-base mb-base space-y-tighter">{children}</ol>,
29-
li: ({ children }) => <li className="text-body-medium pl-tight ml-base">{children}</li>,
30-
31-
// Code
32-
code: ({ inline, children, ...props }) => {
33-
return inline ? (
34-
<code
35-
className="bg-neutral-emphasis text-neutral-content px-tighter py-tightest rounded-smallest font-mono text-body-smaller"
36-
{...props}
37-
>
38-
{children}
39-
</code>
40-
) : (
41-
<code className="font-mono text-body-small" {...props}>
42-
{children}
43-
</code>
44-
);
45-
},
46-
pre: ({ children }) => (
47-
<pre className="bg-neutral-surface-inset border border-neutral-border rounded-small p-base mb-base overflow-x-auto">
48-
{children}
49-
</pre>
50-
),
51-
52-
// Blockquotes
53-
blockquote: ({ children }) => (
54-
<blockquote className="border-l-4 border-primary-border pl-base ml-base mb-base italic text-neutral-content-subtle">
55-
{children}
56-
</blockquote>
57-
),
58-
59-
// Links
60-
a: ({ children, href, ...props }) => (
61-
<a href={href} className="text-primary-content hover:text-primary-content-hover underline" {...props}>
62-
{children}
63-
</a>
64-
),
65-
66-
// Horizontal rule
67-
hr: () => <hr className="border-neutral-border my-wide" />,
68-
69-
// Tables
70-
table: ({ children }) => (
71-
<div className="overflow-x-auto mb-base">
72-
<table className="min-w-full border-collapse border border-neutral-border">{children}</table>
73-
</div>
74-
),
75-
thead: ({ children }) => <thead className="bg-neutral-surface">{children}</thead>,
76-
tbody: ({ children }) => <tbody>{children}</tbody>,
77-
tr: ({ children }) => <tr className="border-b border-neutral-border">{children}</tr>,
78-
th: ({ children }) => (
79-
<th className="border border-neutral-border px-base py-tight text-left font-semibold text-body-medium">
80-
{children}
81-
</th>
82-
),
83-
td: ({ children }) => <td className="border border-neutral-border px-base py-tight text-body-medium">{children}</td>,
84-
85-
// Strong and emphasis
86-
strong: ({ children }) => <strong className="font-bold">{children}</strong>,
87-
em: ({ children }) => <em className="italic">{children}</em>,
88-
89-
// Strikethrough
90-
del: ({ children }) => <del className="line-through text-neutral-content-subtle">{children}</del>,
91-
};
92-
9313
/**
9414
* The `Markdown` element is used to display markdown-formatted text content.
9515
* @example
@@ -153,7 +73,7 @@ const HtxMarkdown = observer(({ item }) => {
15373

15474
return (
15575
<div id={item.idattr} className={item.classname} style={style}>
156-
<ReactMarkdown components={markdownComponents}>{item._value || ""}</ReactMarkdown>
76+
<Markdown text={item._value || ""} allowHtml />
15777
</div>
15878
);
15979
});

web/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
"react-virtualized-auto-sizer": "^1.0.20",
114114
"react-window": "^1.8.11",
115115
"react-window-infinite-loader": "^1.0.5",
116+
"rehype-raw": "^7.0.0",
116117
"sanitize-html": "^2.14.0",
117118
"shadcn": "^2.1.8",
118119
"simplify-js": "^1.2.4",

web/yarn.lock

Lines changed: 100 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11265,6 +11265,46 @@ hasown@^2.0.2:
1126511265
dependencies:
1126611266
function-bind "^1.1.2"
1126711267

11268+
hast-util-from-parse5@^8.0.0:
11269+
version "8.0.3"
11270+
resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz#830a35022fff28c3fea3697a98c2f4cc6b835a2e"
11271+
integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==
11272+
dependencies:
11273+
"@types/hast" "^3.0.0"
11274+
"@types/unist" "^3.0.0"
11275+
devlop "^1.0.0"
11276+
hastscript "^9.0.0"
11277+
property-information "^7.0.0"
11278+
vfile "^6.0.0"
11279+
vfile-location "^5.0.0"
11280+
web-namespaces "^2.0.0"
11281+
11282+
hast-util-parse-selector@^4.0.0:
11283+
version "4.0.0"
11284+
resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz#352879fa86e25616036037dd8931fb5f34cb4a27"
11285+
integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
11286+
dependencies:
11287+
"@types/hast" "^3.0.0"
11288+
11289+
hast-util-raw@^9.0.0:
11290+
version "9.1.0"
11291+
resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-9.1.0.tgz#79b66b26f6f68fb50dfb4716b2cdca90d92adf2e"
11292+
integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==
11293+
dependencies:
11294+
"@types/hast" "^3.0.0"
11295+
"@types/unist" "^3.0.0"
11296+
"@ungap/structured-clone" "^1.0.0"
11297+
hast-util-from-parse5 "^8.0.0"
11298+
hast-util-to-parse5 "^8.0.0"
11299+
html-void-elements "^3.0.0"
11300+
mdast-util-to-hast "^13.0.0"
11301+
parse5 "^7.0.0"
11302+
unist-util-position "^5.0.0"
11303+
unist-util-visit "^5.0.0"
11304+
vfile "^6.0.0"
11305+
web-namespaces "^2.0.0"
11306+
zwitch "^2.0.0"
11307+
1126811308
hast-util-to-jsx-runtime@^2.0.0:
1126911309
version "2.3.6"
1127011310
resolved "https://registry.yarnpkg.com/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz#ff31897aae59f62232e21594eac7ef6b63333e98"
@@ -11286,13 +11326,37 @@ hast-util-to-jsx-runtime@^2.0.0:
1128611326
unist-util-position "^5.0.0"
1128711327
vfile-message "^4.0.0"
1128811328

11329+
hast-util-to-parse5@^8.0.0:
11330+
version "8.0.0"
11331+
resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz#477cd42d278d4f036bc2ea58586130f6f39ee6ed"
11332+
integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==
11333+
dependencies:
11334+
"@types/hast" "^3.0.0"
11335+
comma-separated-tokens "^2.0.0"
11336+
devlop "^1.0.0"
11337+
property-information "^6.0.0"
11338+
space-separated-tokens "^2.0.0"
11339+
web-namespaces "^2.0.0"
11340+
zwitch "^2.0.0"
11341+
1128911342
hast-util-whitespace@^3.0.0:
1129011343
version "3.0.0"
1129111344
resolved "https://registry.yarnpkg.com/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz#7778ed9d3c92dd9e8c5c8f648a49c21fc51cb621"
1129211345
integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
1129311346
dependencies:
1129411347
"@types/hast" "^3.0.0"
1129511348

11349+
hastscript@^9.0.0:
11350+
version "9.0.1"
11351+
resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-9.0.1.tgz#dbc84bef6051d40084342c229c451cd9dc567dff"
11352+
integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==
11353+
dependencies:
11354+
"@types/hast" "^3.0.0"
11355+
comma-separated-tokens "^2.0.0"
11356+
hast-util-parse-selector "^4.0.0"
11357+
property-information "^7.0.0"
11358+
space-separated-tokens "^2.0.0"
11359+
1129611360
he@^1.2.0:
1129711361
version "1.2.0"
1129811362
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -11404,6 +11468,11 @@ html-url-attributes@^3.0.0:
1140411468
resolved "https://registry.yarnpkg.com/html-url-attributes/-/html-url-attributes-3.0.1.tgz#83b052cd5e437071b756cd74ae70f708870c2d87"
1140511469
integrity sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==
1140611470

11471+
html-void-elements@^3.0.0:
11472+
version "3.0.0"
11473+
resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7"
11474+
integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
11475+
1140711476
html-webpack-plugin@^5.5.0:
1140811477
version "5.6.0"
1140911478
resolved "https://registry.yarnpkg.com/html-webpack-plugin/-/html-webpack-plugin-5.6.0.tgz#50a8fa6709245608cb00e811eacecb8e0d7b7ea0"
@@ -13048,10 +13117,10 @@ koa-compose@^4.1.0:
1304813117
resolved "https://registry.yarnpkg.com/koa-compose/-/koa-compose-4.1.0.tgz#507306b9371901db41121c812e923d0d67d3e877"
1304913118
integrity sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==
1305013119

13051-
koa@3.0.1:
13052-
version "3.0.1"
13053-
resolved "https://registry.yarnpkg.com/koa/-/koa-3.0.1.tgz#b211a0f350d1cc6185047671f8ef7e019c16351d"
13054-
integrity sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag==
13120+
koa@3.0.1, koa@>=3.0.3:
13121+
version "3.1.1"
13122+
resolved "https://registry.yarnpkg.com/koa/-/koa-3.1.1.tgz#5550bef74f690412b1f6f2a3cbed851b80ed9809"
13123+
integrity sha512-KDDuvpfqSK0ZKEO2gCPedNjl5wYpfj+HNiuVRlbhd1A88S3M0ySkdf2V/EJ4NWt5dwh5PXCdcenrKK2IQJAxsg==
1305513124
dependencies:
1305613125
accepts "^1.3.8"
1305713126
content-disposition "~0.5.4"
@@ -15804,6 +15873,11 @@ proper-lockfile@^4.1.2:
1580415873
retry "^0.12.0"
1580515874
signal-exit "^3.0.2"
1580615875

15876+
property-information@^6.0.0:
15877+
version "6.5.0"
15878+
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.5.0.tgz#6212fbb52ba757e92ef4fb9d657563b933b7ffec"
15879+
integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
15880+
1580715881
property-information@^7.0.0:
1580815882
version "7.1.0"
1580915883
resolved "https://registry.yarnpkg.com/property-information/-/property-information-7.1.0.tgz#b622e8646e02b580205415586b40804d3e8bfd5d"
@@ -16843,6 +16917,15 @@ regjsparser@^0.9.1:
1684316917
dependencies:
1684416918
jsesc "~0.5.0"
1684516919

16920+
rehype-raw@^7.0.0:
16921+
version "7.0.0"
16922+
resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-7.0.0.tgz#59d7348fd5dbef3807bbaa1d443efd2dd85ecee4"
16923+
integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==
16924+
dependencies:
16925+
"@types/hast" "^3.0.0"
16926+
hast-util-raw "^9.0.0"
16927+
vfile "^6.0.0"
16928+
1684616929
relateurl@^0.2.7:
1684716930
version "0.2.7"
1684816931
resolved "https://registry.yarnpkg.com/relateurl/-/relateurl-0.2.7.tgz#54dbf377e51440aca90a4cd274600d3ff2d888a9"
@@ -19288,6 +19371,14 @@ verror@1.10.0:
1928819371
core-util-is "1.0.2"
1928919372
extsprintf "^1.2.0"
1929019373

19374+
vfile-location@^5.0.0:
19375+
version "5.0.3"
19376+
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-5.0.3.tgz#cb9eacd20f2b6426d19451e0eafa3d0a846225c3"
19377+
integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==
19378+
dependencies:
19379+
"@types/unist" "^3.0.0"
19380+
vfile "^6.0.0"
19381+
1929119382
vfile-message@^4.0.0:
1929219383
version "4.0.3"
1929319384
resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-4.0.3.tgz#87b44dddd7b70f0641c2e3ed0864ba73e2ea8df4"
@@ -19357,6 +19448,11 @@ wcwidth@^1.0.0, wcwidth@^1.0.1:
1935719448
dependencies:
1935819449
defaults "^1.0.3"
1935919450

19451+
web-namespaces@^2.0.0:
19452+
version "2.0.1"
19453+
resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692"
19454+
integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
19455+
1936019456
web-streams-polyfill@^3.0.3:
1936119457
version "3.3.3"
1936219458
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz#2073b91a2fdb1fbfbd401e7de0ac9f8214cecb4b"

0 commit comments

Comments
 (0)