diff --git a/main.go b/main.go index 4d359ab..f673f91 100644 --- a/main.go +++ b/main.go @@ -58,6 +58,10 @@ var template string var mermaidJS string func main() { + // Warnings go to stderr without a timestamp, like the messages from check() + log.SetFlags(0) + log.SetPrefix("mdview: ") + var outfilePtr = flag.String("o", "", "Output filename. (Optional)") var versionPtr = flag.Bool("version", false, "Prints mdview version.") var helpPtr = flag.Bool("help", false, "Prints mdview help message.") @@ -118,10 +122,10 @@ func main() { // Parse markdown to AST doc := md.Parser().Parse(text.NewReader(dat)) - + // Extract title from AST title := getTitleFromAST(doc, dat) - + // Render to HTML var buf bytes.Buffer check(md.Renderer().Render(&buf, dat, doc)) @@ -239,7 +243,7 @@ func getTempDir() string { } if isSnap() { - var tmpdir = os.Getenv("HOME") + "/mdview-temp" + var tmpdir = filepath.Join(os.Getenv("HOME"), "mdview-temp") check(os.MkdirAll(tmpdir, 0700)) return tmpdir } @@ -594,10 +598,10 @@ func hasMermaidBlock(doc ast.Node) bool { // embedMermaidScript adds the embedded Mermaid.js library to the HTML content // Since we set NoScript: true on the Mermaid extender, we need to manually add the script func embedMermaidScript(htmlContent string) string { - // Escape any tags (with closing >) inside the mermaid.js code + // Escape any tags (with closing >) inside the mermaid.js code // to prevent premature script closure. The standard way is to escape the forward slash. escapedMermaidJS := strings.ReplaceAll(mermaidJS, "", "<\\/script>") - + // Add the embedded Mermaid.js and initialization at the end of the content // Initialize mermaid with theme detection initScript := ` @@ -614,6 +618,6 @@ func embedMermaidScript(htmlContent string) string { }); ` inlineScript := fmt.Sprintf("", escapedMermaidJS, initScript) - + return htmlContent + inlineScript } diff --git a/main_test.go b/main_test.go index 2b5b8c8..a807292 100644 --- a/main_test.go +++ b/main_test.go @@ -388,3 +388,71 @@ func TestRemoveStaleTempFiles(t *testing.T) { } } } + +func TestGetTitleFromAST(t *testing.T) { + tests := []struct { + name string + markdown string + want string + }{ + {"simple", "# Hello\n", "Hello"}, + {"first H1 wins", "# First\n\n# Second\n", "First"}, + {"skips other levels", "## Sub\n\n# Main\n", "Main"}, + {"no H1", "## Sub\n\ntext\n", ""}, + {"empty document", "", ""}, + {"inline markup", "# Hello *big* **bold** `code` [link](http://x)\n", "Hello big bold code link"}, + {"setext heading", "Title\n=====\n", "Title"}, + {"surrounding space", "# Spaced \n", "Spaced"}, + {"H1 below a paragraph", "text\n\n# Later\n", "Later"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + source := []byte(tt.markdown) + doc := goldmark.New().Parser().Parse(text.NewReader(source)) + if got := getTitleFromAST(doc, source); got != tt.want { + t.Fatalf("getTitleFromAST = %q, want %q", got, tt.want) + } + }) + } +} + +func TestIsRelativePath(t *testing.T) { + tests := map[string]bool{ + "image.png": true, + "./image.png": true, + "../image.png": true, + "dir/image.png": true, + "my image.png": true, + "": false, + "/etc/image.png": false, + "//cdn.example.com/a.png": false, + "http://example.com/a.png": false, + "https://example.com/a": false, + "data:image/png;base64,AA": false, + "file:///tmp/a.png": false, + "ftp://example.com/a.png": false, + "blob:abc": false, + "mailto:a@b.c": false, + } + for path, want := range tests { + if got := isRelativePath(path); got != want { + t.Errorf("isRelativePath(%q) = %v, want %v", path, got, want) + } + } +} + +func TestTempFileName(t *testing.T) { + dir := t.TempDir() + first := tempFileName(dir, "mdview", ".html") + second := tempFileName(dir, "mdview", ".html") + + if first == second { + t.Fatal("two temp file names were identical") + } + if filepath.Dir(first) != dir { + t.Fatalf("temp file %q is not in %q", first, dir) + } + if !tempFileRegex.MatchString(filepath.Base(first)) { + t.Fatalf("temp file name %q does not match the cleanup pattern", filepath.Base(first)) + } +}