package export import ( "os" "path/filepath" "strings" "testing" ) func testOpts() Options { return Options{ Title: "Doc", Theme: "flexoki", FontDisplay: "Georgia, serif", FontBody: "system-ui, sans-serif", FontMono: "ui-monospace, monospace", Cover: true, } } func TestDocumentEmbedsHouseStyle(t *testing.T) { html, err := Document("Hello world.", testOpts()) if err != nil { t.Fatal(err) } // The vendored doc.css must be baked into the output (self-contained). for _, want := range []string{`
:\n%s", firstLines(html, 3)) } } func TestDocumentRendersMarkdown(t *testing.T) { html, err := Document("# Title\n\nA **bold** word.", testOpts()) if err != nil { t.Fatal(err) } if !strings.Contains(html, "bold") { t.Errorf("markdown not rendered to HTML") } } func TestDocumentWrapsLeadingH1InCover(t *testing.T) { html, err := Document("# Title\n\nSubtitle line.\n\nBody.", testOpts()) if err != nil { t.Fatal(err) } if !strings.Contains(html, `
`) { t.Fatalf("no cover wrapper:\n%s", html) } // The h1 and the immediately following subtitle paragraph live inside cover. cover := between(html, `
`, `
`) if !strings.Contains(cover, "`) { t.Errorf("cover wrapper present though disabled") } } func TestDocumentPageBreakHeadingSuffix(t *testing.T) { html, err := Document("# T\n\n## Section {.page-break}\n\nx", testOpts()) if err != nil { t.Fatal(err) } if !strings.Contains(html, `class="page-break"`) { t.Errorf("{.page-break} suffix not turned into a class:\n%s", html) } if strings.Contains(html, "{.page-break}") { t.Errorf("literal {.page-break} suffix left in heading text") } } func TestPageBreakSuffixDoesNotLeakToEarlierHeading(t *testing.T) { // A leading H1 with no suffix, followed by an H2 carrying {.page-break}: // the suffix must attach to the H2 only, never span to the H1. html, err := Document("# Title\n\nbody\n\n## Section {.page-break}\n\nmore", testOpts()) if err != nil { t.Fatal(err) } if strings.Contains(html, `

\nSection

") { t.Errorf("mismatched heading close tags produced") } if !strings.Contains(html, `

Section

`) { t.Errorf("H2 did not get a clean page-break class:\n%s", html) } } func TestDocumentTaskListItem(t *testing.T) { html, err := Document("- [ ] todo\n- [x] done", testOpts()) if err != nil { t.Fatal(err) } if !strings.Contains(html, "task-list-item") { t.Errorf("checkbox list item missing task-list-item class:\n%s", html) } } func TestDocumentInjectsConfiguredFonts(t *testing.T) { opts := testOpts() opts.FontDisplay = "Charter, serif" opts.FontBody = "Inter, sans-serif" opts.FontMono = "Fira Code, monospace" html, err := Document("x", opts) if err != nil { t.Fatal(err) } // Multi-word names are quoted (Fira Code → "Fira Code"); single-word names // and generics stay bare. for _, want := range []string{"Charter, serif", "Inter, sans-serif", `"Fira Code", monospace`} { if !strings.Contains(html, want) { t.Errorf("configured font %q not injected", want) } } // No licensed face should leak into the output. for _, bad := range []string{"Awke", "Untitled Sans", "Name Mono"} { if strings.Contains(html, bad) { t.Errorf("licensed font %q leaked into export", bad) } } } func TestDocumentStripsFrontmatter(t *testing.T) { md := "---\ntitle: Hi\ntags: [a, b]\n---\n\n# Real Title\n\nBody." html, err := Document(md, testOpts()) if err != nil { t.Fatal(err) } if strings.Contains(html, "tags: [a, b]") { t.Errorf("YAML frontmatter rendered into body:\n%s", html) } if !strings.Contains(html, "Real Title") { t.Errorf("content after frontmatter lost") } } func TestCSSFontStackQuotesMultiWordNames(t *testing.T) { cases := map[string]string{ "Awke": "Awke", // single ident — fine bare "Untitled Sans": `"Untitled Sans"`, // space → must quote "Maple Mono": `"Maple Mono"`, // space → must quote "Inter, system-ui, sans-serif": "Inter, system-ui, sans-serif", "Maple Mono, ui-monospace, monospace": `"Maple Mono", ui-monospace, monospace`, `"Untitled Sans", sans-serif`: `"Untitled Sans", sans-serif`, // already quoted — leave "'Already Quoted'": "'Already Quoted'", // single-quoted — leave "": "", } for in, want := range cases { if got := cssFontStack(in); got != want { t.Errorf("cssFontStack(%q) = %q, want %q", in, got, want) } } } func TestDocumentQuotesMultiWordFonts(t *testing.T) { opts := testOpts() opts.FontBody = "Untitled Sans" opts.FontMono = "Maple Mono" html, err := Document("x", opts) if err != nil { t.Fatal(err) } if !strings.Contains(html, `--font-body:"Untitled Sans"`) { t.Errorf("multi-word body font not quoted in override:\n%s", firstLines(between(html, "--font-display", ""), 1)) } if !strings.Contains(html, `--font-mono:"Maple Mono"`) { t.Errorf("multi-word mono font not quoted in override") } } func TestMapTheme(t *testing.T) { cases := map[string]string{ "flexoki-light": "flexoki", "flexoki-dark": "flexoki-dark", "charm": "flexoki-dark", "": "flexoki", "nonexistent": "flexoki", } for in, want := range cases { if got := MapTheme(in); got != want { t.Errorf("MapTheme(%q) = %q, want %q", in, got, want) } } } func TestOutputPathFromSource(t *testing.T) { // Named files export to the temp dir (basename, .html), never beside the // source — so a vault never accumulates stray HTML next to its markdown. tmp := strings.TrimRight(os.TempDir(), string(os.PathSeparator)) if got := OutputPath("/x/y/note.md", "Title"); got != filepath.Join(tmp, "note.html") { t.Errorf("OutputPath = %q, want %q", got, filepath.Join(tmp, "note.html")) } if got := OutputPath("/x/y/note.markdown", "Title"); got != filepath.Join(tmp, "note.html") { t.Errorf("OutputPath = %q, want %q", got, filepath.Join(tmp, "note.html")) } } func TestOutputPathUnnamedUsesTempSlug(t *testing.T) { got := OutputPath("", "My Great Note!") if filepath.Dir(got) != strings.TrimRight(os.TempDir(), string(os.PathSeparator)) { t.Errorf("unnamed export should land in temp dir, got %q", got) } base := filepath.Base(got) if base != "my-great-note.html" { t.Errorf("slug base = %q, want my-great-note.html", base) } } func TestWriteCreatesSelfContainedFile(t *testing.T) { dir := t.TempDir() src := filepath.Join(dir, "doc.md") if err := os.WriteFile(src, []byte("# Hi\n\nbody"), 0o644); err != nil { t.Fatal(err) } out, err := Write(src, "# Hi\n\nbody", testOpts()) if err != nil { t.Fatal(err) } wantOut := filepath.Join(strings.TrimRight(os.TempDir(), string(os.PathSeparator)), "doc.html") if out != wantOut { t.Errorf("out = %q, want %q (temp dir, not beside source)", out, wantOut) } if filepath.Dir(out) == dir { t.Errorf("export landed beside source %q; should go to temp", dir) } data, err := os.ReadFile(out) if err != nil { t.Fatal(err) } if !strings.Contains(string(data), `
n { lines = lines[:n] } return strings.Join(lines, "\n") } func between(s, start, end string) string { i := strings.Index(s, start) if i < 0 { return "" } i += len(start) j := strings.Index(s[i:], end) if j < 0 { return s[i:] } return s[i : i+j] }