1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
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(), "-")
}
|