Add mermaid suport
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
|
import { createRequire } from "node:module";
|
||||||
import { fileURLToPath, URL } from "node:url";
|
import { fileURLToPath, URL } from "node:url";
|
||||||
|
|
||||||
import tailwindcss from "@tailwindcss/vite";
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -54,6 +57,7 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
"#": fileURLToPath(new URL("./src", import.meta.url)),
|
"#": fileURLToPath(new URL("./src", import.meta.url)),
|
||||||
|
"mermaid": require.resolve("mermaid/dist/mermaid.esm.min.mjs"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -27,6 +27,7 @@ require (
|
|||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
github.com/vektah/gqlparser/v2 v2.5.32
|
github.com/vektah/gqlparser/v2 v2.5.32
|
||||||
github.com/yuin/goldmark v1.7.16
|
github.com/yuin/goldmark v1.7.16
|
||||||
|
go.abhg.dev/goldmark/mermaid v0.6.0
|
||||||
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712
|
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712
|
||||||
go.gearno.de/kit v0.1.1
|
go.gearno.de/kit v0.1.1
|
||||||
go.gearno.de/x/ref v0.0.0-20260216110753-a700c951377c
|
go.gearno.de/x/ref v0.0.0-20260216110753-a700c951377c
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -258,6 +258,8 @@ github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zI
|
|||||||
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
|
||||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
|
go.abhg.dev/goldmark/mermaid v0.6.0 h1:VvkYFWuOjD6cmSBVJpLAtzpVCGM1h0B7/DQ9IzERwzY=
|
||||||
|
go.abhg.dev/goldmark/mermaid v0.6.0/go.mod h1:uMc+PcnIH2NVL7zjH10Q1wr7hL3+4n4jUMifhyBYB9I=
|
||||||
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712 h1:J5ccbcxFuwxe6Oa9fVi9FqQOo+n17ni4wbl9t4NuEzc=
|
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712 h1:J5ccbcxFuwxe6Oa9fVi9FqQOo+n17ni4wbl9t4NuEzc=
|
||||||
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712/go.mod h1:fnIIvKO9QnsyLO3ZJLJT3r8KZv/p0FOeT5eZKilYWXg=
|
go.gearno.de/crypto/uuid v0.1.1-0.20251208105319-3f587312a712/go.mod h1:fnIIvKO9QnsyLO3ZJLJT3r8KZv/p0FOeT5eZKilYWXg=
|
||||||
go.gearno.de/kit v0.1.1 h1:QuBZCZ/h2Eyh6DjjR6CGjkdsab/ztHz6xUiIk0FeREE=
|
go.gearno.de/kit v0.1.1 h1:QuBZCZ/h2Eyh6DjjR6CGjkdsab/ztHz6xUiIk0FeREE=
|
||||||
|
|||||||
1231
package-lock.json
generated
1231
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -24,6 +24,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.1.13",
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
"mermaid": "^11.13.0",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-intersection-observer": "^9.16.0",
|
"react-intersection-observer": "^9.16.0",
|
||||||
"react-markdown": "^10.1.0",
|
"react-markdown": "^10.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import { isValidElement } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
import { MermaidDiagram } from "./MermaidDiagram";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
content: string;
|
content: string;
|
||||||
};
|
};
|
||||||
@@ -23,19 +26,31 @@ export function Markdown({ content }: Props) {
|
|||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
),
|
),
|
||||||
pre: ({ children, ...props }) => (
|
pre: ({ children, ...props }) => {
|
||||||
|
const child = isValidElement<{
|
||||||
|
className?: string;
|
||||||
|
children?: string;
|
||||||
|
}>(children)
|
||||||
|
? children
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
child?.type === "code"
|
||||||
|
&& child.props.className === "language-mermaid"
|
||||||
|
&& typeof child.props.children === "string"
|
||||||
|
) {
|
||||||
|
return <MermaidDiagram chart={child.props.children} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
<pre
|
<pre
|
||||||
className="border border-border-solid rounded p-4 bg-transparent font-mono text-sm overflow-x-auto text-inherit"
|
className="border border-border-solid rounded p-4 bg-transparent font-mono text-sm overflow-x-auto text-inherit"
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
),
|
);
|
||||||
code: ({ children, ...props }) => (
|
},
|
||||||
<code className="font-mono text-sm text-inherit" {...props}>
|
|
||||||
{children}
|
|
||||||
</code>
|
|
||||||
),
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
51
packages/ui/src/Atoms/Markdown/MermaidDiagram.tsx
Normal file
51
packages/ui/src/Atoms/Markdown/MermaidDiagram.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import mermaid from "mermaid";
|
||||||
|
import { useEffect, useId, useState } from "react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
chart: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function MermaidDiagram({ chart }: Props) {
|
||||||
|
const id = useId().replace(/:/g, "");
|
||||||
|
const [svg, setSvg] = useState<string | null>(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
mermaid.initialize({ startOnLoad: false, theme: "neutral" });
|
||||||
|
|
||||||
|
mermaid
|
||||||
|
.render(`mermaid-${id}`, chart.trim())
|
||||||
|
.then((result) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setSvg(result.svg);
|
||||||
|
setError(null);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setError(err instanceof Error ? err.message : String(err));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [chart, id]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<pre className="border border-border-solid rounded p-4 bg-transparent font-mono text-sm overflow-x-auto text-inherit">
|
||||||
|
<code>{chart}</code>
|
||||||
|
</pre>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex justify-center my-4"
|
||||||
|
dangerouslySetInnerHTML={svg ? { __html: svg } : undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -26,10 +26,14 @@ import (
|
|||||||
"github.com/yuin/goldmark"
|
"github.com/yuin/goldmark"
|
||||||
"github.com/yuin/goldmark/extension"
|
"github.com/yuin/goldmark/extension"
|
||||||
gmhtml "github.com/yuin/goldmark/renderer/html"
|
gmhtml "github.com/yuin/goldmark/renderer/html"
|
||||||
|
"go.abhg.dev/goldmark/mermaid"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
//go:embed mermaid.min.js
|
||||||
|
mermaidJSSource string
|
||||||
|
|
||||||
//go:embed template.html
|
//go:embed template.html
|
||||||
htmlTemplateContent string
|
htmlTemplateContent string
|
||||||
|
|
||||||
@@ -72,7 +76,13 @@ var (
|
|||||||
},
|
},
|
||||||
"formatContent": func(content string) template.HTML {
|
"formatContent": func(content string) template.HTML {
|
||||||
md := goldmark.New(
|
md := goldmark.New(
|
||||||
goldmark.WithExtensions(extension.Table),
|
goldmark.WithExtensions(
|
||||||
|
extension.Table,
|
||||||
|
&mermaid.Extender{
|
||||||
|
RenderMode: mermaid.RenderModeClient,
|
||||||
|
NoScript: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
goldmark.WithRendererOptions(
|
goldmark.WithRendererOptions(
|
||||||
gmhtml.WithUnsafe(),
|
gmhtml.WithUnsafe(),
|
||||||
),
|
),
|
||||||
@@ -209,6 +219,7 @@ type (
|
|||||||
PublishedAt *time.Time
|
PublishedAt *time.Time
|
||||||
Signatures []SignatureData
|
Signatures []SignatureData
|
||||||
CompanyHorizontalLogoBase64 string
|
CompanyHorizontalLogoBase64 string
|
||||||
|
MermaidJS template.JS
|
||||||
}
|
}
|
||||||
|
|
||||||
SignatureData struct {
|
SignatureData struct {
|
||||||
@@ -321,6 +332,8 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func RenderHTML(data DocumentData) ([]byte, error) {
|
func RenderHTML(data DocumentData) ([]byte, error) {
|
||||||
|
data.MermaidJS = template.JS(mermaidJSSource)
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := documentTemplate.Execute(&buf, data); err != nil {
|
if err := documentTemplate.Execute(&buf, data); err != nil {
|
||||||
return nil, fmt.Errorf("cannot execute template: %w", err)
|
return nil, fmt.Errorf("cannot execute template: %w", err)
|
||||||
|
|||||||
2843
pkg/docgen/mermaid.min.js
vendored
Normal file
2843
pkg/docgen/mermaid.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -341,7 +341,31 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.document-content pre.mermaid {
|
||||||
|
border: none;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.document-content pre.mermaid svg {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
<script>{{.MermaidJS}}</script>
|
||||||
|
<script>
|
||||||
|
window.__mermaidReady = false;
|
||||||
|
mermaid.initialize({ startOnLoad: false, theme: 'neutral' });
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
mermaid.run().then(function () {
|
||||||
|
window.__mermaidReady = true;
|
||||||
|
}).catch(function () {
|
||||||
|
window.__mermaidReady = true;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="document-container">
|
<div class="document-container">
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/page"
|
"github.com/chromedp/cdproto/page"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
@@ -49,6 +50,8 @@ type (
|
|||||||
MarginRight Margin
|
MarginRight Margin
|
||||||
PrintBackground bool
|
PrintBackground bool
|
||||||
Scale float64 // Print scale (0.1 to 2.0)
|
Scale float64 // Print scale (0.1 to 2.0)
|
||||||
|
WaitForExpression string // Optional JS expression polled until true before printing
|
||||||
|
WaitForExpressionTimeout time.Duration // Max time to wait (default 15s)
|
||||||
}
|
}
|
||||||
|
|
||||||
Option func(*Converter)
|
Option func(*Converter)
|
||||||
@@ -162,6 +165,30 @@ func (c *Converter) GeneratePDF(ctx context.Context, htmlDocument []byte, cfg Re
|
|||||||
|
|
||||||
htmlContent := string(htmlDocument)
|
htmlContent := string(htmlDocument)
|
||||||
|
|
||||||
|
waitTimeout := cfg.WaitForExpressionTimeout
|
||||||
|
if waitTimeout <= 0 {
|
||||||
|
waitTimeout = 15 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
waitForExpr := chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if cfg.WaitForExpression == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(waitTimeout)
|
||||||
|
for time.Now().Before(deadline) {
|
||||||
|
var ready bool
|
||||||
|
if err := chromedp.Evaluate(cfg.WaitForExpression, &ready).Do(ctx); err != nil {
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ready {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
return nil // proceed even on timeout
|
||||||
|
})
|
||||||
|
|
||||||
err := chromedp.Run(
|
err := chromedp.Run(
|
||||||
ctx,
|
ctx,
|
||||||
chromedp.Navigate("about:blank"),
|
chromedp.Navigate("about:blank"),
|
||||||
@@ -178,6 +205,7 @@ func (c *Converter) GeneratePDF(ctx context.Context, htmlDocument []byte, cfg Re
|
|||||||
}),
|
}),
|
||||||
chromedp.WaitReady("body"),
|
chromedp.WaitReady("body"),
|
||||||
chromedp.ActionFunc(waitUntilDocumentReady),
|
chromedp.ActionFunc(waitUntilDocumentReady),
|
||||||
|
waitForExpr,
|
||||||
chromedp.ActionFunc(
|
chromedp.ActionFunc(
|
||||||
func(ctx context.Context) (err error) {
|
func(ctx context.Context) (err error) {
|
||||||
pdfBytes, _, err = page.PrintToPDF().
|
pdfBytes, _, err = page.PrintToPDF().
|
||||||
|
|||||||
@@ -1892,6 +1892,7 @@ func exportDocumentPDF(
|
|||||||
MarginRight: html2pdf.NewMarginInches(1.0),
|
MarginRight: html2pdf.NewMarginInches(1.0),
|
||||||
PrintBackground: true,
|
PrintBackground: true,
|
||||||
Scale: 1.0,
|
Scale: 1.0,
|
||||||
|
WaitForExpression: "window.__mermaidReady === true",
|
||||||
}
|
}
|
||||||
|
|
||||||
pdfReader, err := html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
|
pdfReader, err := html2pdfConverter.GeneratePDF(ctx, htmlContent, cfg)
|
||||||
|
|||||||
Reference in New Issue
Block a user