package main import ( _ "embed" "errors" "flag" "fmt" "log" "os" "os/signal" "path" "path/filepath" "strings" "syscall" "git.foosoft.net/alex/goldsmith" "git.foosoft.net/alex/goldsmith/filters/operator" "git.foosoft.net/alex/goldsmith/filters/wildcard" "git.foosoft.net/alex/goldsmith/plugins/document" "git.foosoft.net/alex/goldsmith/plugins/frontmatter" "git.foosoft.net/alex/goldsmith/plugins/livejs" "git.foosoft.net/alex/goldsmith/plugins/markdown" "github.com/PuerkitoBio/goquery" "github.com/fsnotify/fsnotify" "github.com/skratchdot/open-golang/open" "github.com/yuin/goldmark" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" "github.com/yuin/goldmark/renderer/html" ) //go:embed css/github-markdown.css var githubStyle string //go:embed css/github-fixup.css var githubFixup string func watch(dir string, watcher *fsnotify.Watcher) error { watcher.Add(dir) items, err := os.ReadDir(dir) if err != nil { return err } for _, item := range items { fullPath := path.Join(dir, item.Name()) if item.IsDir() { watch(fullPath, watcher) } else { watcher.Add(fullPath) } } return nil } func builder(dir string, callback func(bool) error) error { watcher, err := fsnotify.NewWatcher() if err != nil { return err } if err := watch(dir, watcher); err != nil { return err } if err := callback(true); err != nil { return err } signaler := make(chan os.Signal, 1) signal.Notify( signaler, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, ) for { select { case event := <-watcher.Events: if err := callback(false); err != nil { return err } if event.Op&fsnotify.Create == fsnotify.Create { if info, _ := os.Stat(event.Name); info != nil { if info.IsDir() { watch(event.Name, watcher) } else { watcher.Add(event.Name) } } } case <-signaler: return nil } } } func build(sourceDir, targetDir string) error { log.Println("building...") embedCss := func(file *goldsmith.File, doc *goquery.Document) error { var styleBuilder strings.Builder styleBuilder.WriteString("") doc.Find("body").AddClass("markdown-body") doc.Find("head").SetHtml(styleBuilder.String()) return nil } allowedPaths := []string{ "**/*.gif", "**/*.html", "**/*.jpeg", "**/*.jpg", "**/*.md", "**/*.png", "**/*.svg", } forbiddenPaths := []string{ "**/.*/**", } gm := goldmark.New( goldmark.WithExtensions(extension.GFM, extension.Typographer), goldmark.WithParserOptions(parser.WithAutoHeadingID()), goldmark.WithRendererOptions(html.WithUnsafe()), ) gs := goldsmith.Goldsmith{Clean: true} errs := gs.Begin(sourceDir). FilterPush(wildcard.New(allowedPaths...)). FilterPush(operator.Not(wildcard.New(forbiddenPaths...))). Chain(frontmatter.New()). Chain(markdown.NewWithGoldmark(gm)). Chain(livejs.New()). Chain(document.New(embedCss)). End(targetDir) if len(errs) > 0 { return errs[0] } return nil } func run(path string) error { switch strings.ToLower(filepath.Ext(path)) { case ".md", ".markdown": break default: return errors.New("unexpected file type") } if info, err := os.Stat(path); err != nil { return err } else if info.IsDir() { return errors.New("unexpected directory") } targetDir, err := os.MkdirTemp("", "mdv-*") if err != nil { return err } defer os.RemoveAll(targetDir) sourceDir := filepath.Dir(path) builder(sourceDir, func(first bool) error { if err := build(sourceDir, targetDir); err != nil { return err } if first { var ( mdName = filepath.Base(path) mdExt = filepath.Ext(mdName) htmlPath = filepath.Join(targetDir, mdName[:len(mdName)-len(mdExt)]+".html") ) if _, err := os.Stat(htmlPath); err != nil { return err } if err := open.Start(htmlPath); err != nil { return err } } return nil }) return nil } func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage %s [options] \n\n", filepath.Base(os.Args[0])) fmt.Fprintln(os.Stderr, "Parameters:") flag.PrintDefaults() } flag.Parse() if flag.NArg() != 1 { flag.Usage() os.Exit(2) } if err := run(flag.Arg(0)); err != nil { log.Fatal(err) } }