diff --git a/README.md b/README.md index bdb7718..7f858b1 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ Export is **pluggable** — formats register themselves at startup: | **docx** | Microsoft Word document with rendered Markdown, contents, and embedded images | None | | **pdf** | Multi-page PDF with table of contents | Chrome, Edge, or Chromium on `$PATH` | -DOCX export uses the MIT-licensed [docxgo](https://github.com/mmonterroca/docxgo) library, with no Word installation or external converter required. It preserves headings, emphasis, lists, task lists, quotes, code blocks, tables, and PNG/JPEG/GIF images. External links remain clickable; wikilinks to exported pages become in-document hyperlinks, while wikilinks outside the export are shown as non-clickable dark gray underlined text. Mermaid blocks are rendered as embedded PNGs when Chromium and the local Mermaid bundle are available, with a code-block fallback otherwise. +DOCX export uses the MIT-licensed [docxgo](https://github.com/mmonterroca/docxgo) library, with no Word installation or external converter required. It preserves headings, emphasis, lists, task lists, quotes, code blocks, tables, and PNG/JPEG/GIF images. External links remain clickable; wikilinks to exported pages become in-document hyperlinks, while wikilinks outside the export are shown as non-clickable dark gray underlined text. Mermaid blocks are rendered as embedded PNGs when a supported Chromium-based browser is available; release binaries include the Mermaid browser bundle, with a code-block fallback if rendering fails. PDF export uses [chromedp](https://github.com/chromedp/chromedp) to render markdown → HTML → PDF via a headless browser. It includes a clickable TOC, embedded images, and a print-friendly stylesheet. The PDF sharer only registers if a supported browser is detected — no browser, no PDF option. diff --git a/internal/share/pdf.go b/internal/share/pdf.go index 600ae19..501fd02 100644 --- a/internal/share/pdf.go +++ b/internal/share/pdf.go @@ -7,6 +7,7 @@ import ( "fmt" "html" "io" + "io/fs" "log/slog" "net" "net/http" @@ -17,6 +18,7 @@ import ( "strings" "time" + "github.com/aniongithub/mind-map/webui" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" "github.com/yuin/goldmark" @@ -36,9 +38,11 @@ func init() { } } -func (p *PDFSharer) Name() string { return "pdf" } -func (p *PDFSharer) Description() string { return "Multi-page PDF with table of contents (requires Chrome/Edge)" } -func (p *PDFSharer) ContentType() string { return "application/pdf" } +func (p *PDFSharer) Name() string { return "pdf" } +func (p *PDFSharer) Description() string { + return "Multi-page PDF with table of contents (requires Chrome/Edge)" +} +func (p *PDFSharer) ContentType() string { return "application/pdf" } func (p *PDFSharer) FileExtension() string { return ".pdf" } func (p *PDFSharer) Settings() SharerSettings { @@ -226,9 +230,25 @@ func pagesHaveMermaid(pages []Page) bool { return false } +// embeddedMermaidJS reads the Mermaid bundle copied into the embedded WebUI. +func embeddedMermaidJS() []byte { + dist := webui.DistFS() + if dist == nil { + return nil + } + data, err := fs.ReadFile(dist, "mermaid.min.js") + if err != nil { + return nil + } + return data +} + // findMermaidJS locates and reads the mermaid.min.js bundle. -// It checks common locations relative to the working directory and executable. func findMermaidJS() []byte { + if data := embeddedMermaidJS(); data != nil { + return data + } + candidates := []string{ // Development: relative to project root (cwd) "webui/node_modules/mermaid/dist/mermaid.min.js", @@ -347,29 +367,41 @@ func paperDimensions(size string) (width, height float64) { } } -// findBrowser scans for a Chromium-based browser on the system. -func findBrowser() string { - // Platform-specific paths to try - var candidates []string - - switch runtime.GOOS { +func browserCandidates(goos, homeDir string) []string { + switch goos { case "darwin": - candidates = []string{ - "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", - "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", - "/Applications/Chromium.app/Contents/MacOS/Chromium", - "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + applications := []string{ + "Google Chrome.app/Contents/MacOS/Google Chrome", + "Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "Chromium.app/Contents/MacOS/Chromium", + "Brave Browser.app/Contents/MacOS/Brave Browser", + } + candidates := make([]string, 0, len(applications)*2) + for _, application := range applications { + candidates = append(candidates, filepath.Join("/Applications", application)) + } + if homeDir != "" { + for _, application := range applications { + candidates = append(candidates, filepath.Join(homeDir, "Applications", application)) + } } + return candidates case "windows": - candidates = []string{ + return []string{ `C:\Program Files\Google\Chrome\Application\chrome.exe`, `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, `C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`, `C:\Program Files\Microsoft\Edge\Application\msedge.exe`, } - default: // linux and others - candidates = []string{} + default: + return nil } +} + +// findBrowser scans for a Chromium-based browser on the system. +func findBrowser() string { + homeDir, _ := os.UserHomeDir() + candidates := browserCandidates(runtime.GOOS, homeDir) // Also check $PATH for common names pathNames := []string{ diff --git a/internal/share/pdf_test.go b/internal/share/pdf_test.go index 77cfcb5..1c35908 100644 --- a/internal/share/pdf_test.go +++ b/internal/share/pdf_test.go @@ -6,6 +6,7 @@ import ( "fmt" "net" "net/http" + "slices" "strings" "testing" "time" @@ -190,3 +191,30 @@ func TestPDFRenderHTMLNoMermaid(t *testing.T) { t.Error("HTML should still contain data-mermaid-done signal for consistency") } } + +func TestEmbeddedMermaidJS(t *testing.T) { + data := embeddedMermaidJS() + if len(data) == 0 { + t.Fatal("embedded WebUI does not contain mermaid.min.js") + } + if !bytes.Contains(data, []byte("mermaid")) { + t.Fatal("embedded mermaid.min.js does not look like a Mermaid bundle") + } +} + +func TestDarwinBrowserCandidatesIncludeUserApplications(t *testing.T) { + got := browserCandidates("darwin", "/Users/tester") + want := []string{ + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + "/Users/tester/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + "/Users/tester/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", + "/Users/tester/Applications/Chromium.app/Contents/MacOS/Chromium", + "/Users/tester/Applications/Brave Browser.app/Contents/MacOS/Brave Browser", + } + if !slices.Equal(got, want) { + t.Fatalf("browserCandidates() = %#v, want %#v", got, want) + } +} diff --git a/webui/webpack.config.js b/webui/webpack.config.js index 8fdac07..e4931b3 100644 --- a/webui/webpack.config.js +++ b/webui/webpack.config.js @@ -1,6 +1,7 @@ const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry: './src/index.tsx', @@ -39,5 +40,13 @@ module.exports = { favicon: path.resolve(__dirname, 'src/assets/favicon.svg'), }), new MiniCssExtractPlugin({ filename: 'index.css' }), + new CopyWebpackPlugin({ + patterns: [ + { + from: path.resolve(__dirname, 'node_modules/mermaid/dist/mermaid.min.js'), + to: 'mermaid.min.js', + }, + ], + }), ], };