phase2: markdown converter with goldmark, chroma, and ASCII-translit anchors

This commit is contained in:
Sergey Filkin
2026-04-18 11:47:18 +03:00
parent cab04768b5
commit 8deba3627f
39 changed files with 5662 additions and 0 deletions
+162
View File
@@ -0,0 +1,162 @@
package converter
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
"github.com/fserg/md-to-html/web/template"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
func TestGolden(t *testing.T) {
c := newTestConverter(t)
update := os.Getenv("UPDATE_GOLDEN") == "1"
entries, err := os.ReadDir("testdata")
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
name := entry.Name()
if entry.IsDir() || !strings.HasSuffix(name, ".md") {
continue
}
t.Run(name, func(t *testing.T) {
md, err := os.ReadFile(filepath.Join("testdata", name))
if err != nil {
t.Fatal(err)
}
wantPath := filepath.Join("testdata", strings.TrimSuffix(name, ".md")+".html")
got, err := c.Convert(md, "Document")
if err != nil {
t.Fatal(err)
}
for _, forbidden := range []string{"http://", "https://", "cdn.", "googleapis.com"} {
if bytes.Contains(got.HTML, []byte(forbidden)) {
t.Fatalf("generated HTML contains forbidden external resource marker %q", forbidden)
}
}
if update {
if err := os.WriteFile(wantPath, got.HTML, 0o644); err != nil {
t.Fatal(err)
}
return
}
want, err := os.ReadFile(wantPath)
if err != nil {
t.Fatalf("missing golden %s; run UPDATE_GOLDEN=1", wantPath)
}
if !bytes.Equal(got.HTML, want) {
t.Errorf("mismatch: run UPDATE_GOLDEN=1 go test ./internal/converter/... to refresh")
}
})
}
}
func TestTranslitSlug(t *testing.T) {
tests := []struct {
name string
in string
want string
used map[string]int
}{
{name: "cyrillic", in: "Установка", want: "ustanovka", used: map[string]int{}},
{name: "collision first", in: "Install", want: "install", used: map[string]int{}},
{name: "collision second", in: "Install", want: "install-1", used: map[string]int{"install": 1}},
{name: "cyrillic translit", in: "Сетап", want: "setap", used: map[string]int{}},
{name: "empty fallback", in: "!!!", want: "section", used: map[string]int{}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := translitSlug(tt.in, tt.used)
if got != tt.want {
t.Fatalf("translitSlug(%q) = %q, want %q", tt.in, got, tt.want)
}
})
}
}
func TestExtractHeadingText(t *testing.T) {
c := newTestConverter(t)
src := []byte("## [API](https://example.com) `go fmt` https://example.com :rocket:\n")
doc := c.md.Parser().Parse(text.NewReader(src))
var heading *ast.Heading
_ = ast.Walk(doc, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
if !entering {
return ast.WalkContinue, nil
}
if h, ok := n.(*ast.Heading); ok {
heading = h
return ast.WalkStop, nil
}
return ast.WalkContinue, nil
})
if heading == nil {
t.Fatal("heading not found")
}
got := extractHeadingText(heading, src)
want := "API go fmt https://example.com 🚀"
if got != want {
t.Fatalf("extractHeadingText() = %q, want %q", got, want)
}
}
func TestConvertTitleFromFirstHeading(t *testing.T) {
c := newTestConverter(t)
result, err := c.Convert([]byte("# Hello\n\nParagraph"), "fallback")
if err != nil {
t.Fatal(err)
}
if result.Title != "Hello" {
t.Fatalf("result.Title = %q, want %q", result.Title, "Hello")
}
if !bytes.Contains(result.HTML, []byte("<title>Hello</title>")) {
t.Fatalf("expected HTML title to contain Hello")
}
}
func TestConvertTitleFallback(t *testing.T) {
c := newTestConverter(t)
result, err := c.Convert([]byte("Paragraph only"), "fallback")
if err != nil {
t.Fatal(err)
}
if result.Title != "fallback" {
t.Fatalf("result.Title = %q, want %q", result.Title, "fallback")
}
if !bytes.Contains(result.HTML, []byte("<h1>fallback</h1>")) {
t.Fatalf("expected fallback h1 to be injected")
}
}
func newTestConverter(t *testing.T) *Converter {
t.Helper()
c, err := New(webtemplate.FS)
if err != nil {
t.Fatal(err)
}
return c
}