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))
+ }
+}