package export import ( "os" "os/exec" "path/filepath" "regexp" "runtime" "strings" ) // OutputPath is where an export of srcPath lands: always the OS temp dir, never // beside the source. A named file keeps its basename (extension swapped for // .html); an unnamed buffer (empty srcPath) is named from a slug of the title. // Exports are transient artifacts for the Print → Save as PDF flow, so keeping // them out of the source tree stops a vault from filling up with stray HTML. func OutputPath(srcPath, title string) string { var name string if srcPath != "" { base := filepath.Base(srcPath) name = strings.TrimSuffix(base, filepath.Ext(base)) } else { name = slug(title) } if name == "" { name = "document" } return filepath.Join(os.TempDir(), name+".html") } // Write renders markdown to a self-contained HTML document and writes it to // the export path for srcPath, returning that path. func Write(srcPath, markdown string, opts Options) (string, error) { doc, err := Document(markdown, opts) if err != nil { return "", err } out := OutputPath(srcPath, opts.Title) if err := os.WriteFile(out, []byte(doc), 0o644); err != nil { return "", err } return out, nil } var atxH1RE = regexp.MustCompile(`(?m)^#\s+(.+?)\s*#*\s*$`) // Title is the document title for an export: the first `# H1` in the markdown, // else the source filename without extension, else "Untitled". func Title(srcPath, markdown string) string { if m := atxH1RE.FindStringSubmatch(stripFrontmatter(markdown)); m != nil { if t := strings.TrimSpace(m[1]); t != "" { return t } } if srcPath != "" { base := filepath.Base(srcPath) return strings.TrimSuffix(base, filepath.Ext(base)) } return "Untitled" } // OpenInBrowser opens path in the user's default browser, best-effort. It // returns any error starting the program; the browser then runs detached. func OpenInBrowser(path string) error { name, args := browserCommand(path) return exec.Command(name, args...).Start() } // browserCommand returns the platform program (and args) that opens path in // the user's default browser. func browserCommand(path string) (string, []string) { switch runtime.GOOS { case "darwin": return "open", []string{path} case "windows": return "cmd", []string{"/c", "start", "", path} default: // linux, *bsd return "xdg-open", []string{path} } } // slug turns a title into a filename-safe lowercase token. func slug(title string) string { var b strings.Builder prevDash := false for _, r := range strings.ToLower(title) { switch { case (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9'): b.WriteRune(r) prevDash = false default: if !prevDash && b.Len() > 0 { b.WriteByte('-') prevDash = true } } } return strings.Trim(b.String(), "-") }