summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--etc/pubrefman/pubrefman2.go510
1 files changed, 510 insertions, 0 deletions
diff --git a/etc/pubrefman/pubrefman2.go b/etc/pubrefman/pubrefman2.go
new file mode 100644
index 00000000..14578181
--- /dev/null
+++ b/etc/pubrefman/pubrefman2.go
@@ -0,0 +1,510 @@
+// Copyright 2024 Staysail Systems, Inc.
+//
+// This software is supplied under the terms of the MIT License, a
+// copy of which should be located in the distribution where this
+// file was obtained (LICENSE.txt). A copy of the license may also be
+// found online at https://opensource.org/licenses/MIT.
+//
+
+// This tool uses a local in memory copy of git, and a docker
+// installation, to format the man pages. The documentation will be
+// pushed up into a branch.
+
+package main
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+ "os/exec"
+ "path"
+ "sort"
+ "strings"
+ "time"
+
+ "github.com/go-git/go-billy/v5"
+ "github.com/go-git/go-billy/v5/memfs"
+ "github.com/go-git/go-git/v5"
+ "github.com/go-git/go-git/v5/plumbing"
+ "github.com/go-git/go-git/v5/plumbing/object"
+ "github.com/go-git/go-git/v5/storage/memory"
+ "github.com/google/uuid"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+type Configuration struct {
+ Version string
+ Debug bool
+ Trace bool
+ Quiet bool
+ DryRun bool
+ Author string
+ Email string
+ Url string
+ Message string
+}
+
+var Config Configuration
+
+func init() {
+ flag.StringVar(&Config.Version, "v", "tip", "Version to publish")
+ flag.BoolVar(&Config.Debug, "d", false, "Enable debugging")
+ flag.BoolVar(&Config.Trace, "t", false, "Enable tracing")
+ flag.BoolVar(&Config.Quiet, "q", false, "Run quietly")
+ flag.BoolVar(&Config.DryRun, "n", false, "Dry run, does not push changes")
+ flag.StringVar(&Config.Url, "u", "ssh://git@github.com/nanomsg/nng.git", "URL of repo to publish from")
+ flag.StringVar(&Config.Email, "E", "info@staysail.tech", "Author email for commit")
+ flag.StringVar(&Config.Author, "A", "Staysail Systems, Inc.", "Author name for commit")
+ flag.StringVar(&Config.Message, "m", "", "Commit message")
+}
+
+func (g *Global) CheckError(err error, prefix string, args ...interface{}) {
+ if err == nil {
+ g.Log.TRACE.Printf("%s: ok", fmt.Sprintf(prefix, args...))
+ return
+ }
+ g.Log.FATAL.Fatalf("Error: %s: %v", fmt.Sprintf(prefix, args...), err)
+}
+
+func (g *Global) Fatal(format string, args ...interface{}) {
+ g.Log.FATAL.Fatalf("Error: %s", fmt.Sprintf(format, args...))
+}
+
+type Section struct {
+ Name string
+ Synopsis string
+ Description string
+ Pages []*Page
+}
+
+type Page struct {
+ Name string
+ Section string
+ Description string
+ Content string
+}
+
+type Global struct {
+ Config Configuration
+ SrcFs billy.Filesystem
+ DstFs billy.Filesystem
+ DstDir string
+ Sections map[string]*Section
+ Pages map[string]*Page
+ Repo *git.Repository
+ Index string
+ ToC string
+ Added map[string]bool
+ WorkTree *git.Worktree
+ Branch string
+ OldHash plumbing.Hash
+ NewHash plumbing.Hash
+ Log *jww.Notepad
+}
+
+func (g *Global) Init() {
+ g.Config = Config
+ g.Sections = make(map[string]*Section)
+ g.Pages = make(map[string]*Page)
+ g.Added = make(map[string]bool)
+ g.SrcFs = memfs.New()
+ g.DstFs = memfs.New()
+ g.DstDir = path.Join("man", g.Config.Version)
+ thresh := jww.LevelInfo
+ if g.Config.Quiet {
+ thresh = jww.LevelError
+ }
+ if g.Config.Debug {
+ thresh = jww.LevelDebug
+ }
+ if g.Config.Trace {
+ thresh = jww.LevelTrace
+ }
+ g.Log = jww.NewNotepad(thresh, thresh, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime)
+}
+
+func (g *Global) Destroy() {
+}
+
+func (g *Global) Debug(format string, args ...interface{}) {
+ g.Log.DEBUG.Printf(format, args...)
+}
+
+func (g *Global) Print(format string, args ...interface{}) {
+ g.Log.INFO.Printf(format, args...)
+}
+
+func (g *Global) CloneSource() {
+ tag := g.Config.Version
+ if tag == "" || tag == "tip" {
+ tag = "master"
+ }
+ ref := plumbing.NewBranchReferenceName(tag)
+ if strings.HasPrefix(tag, "v") {
+ ref = plumbing.NewTagReferenceName(tag)
+ }
+ ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+ defer cancel()
+
+ now := time.Now()
+ _, err := git.CloneContext(ctx, memory.NewStorage(), g.SrcFs, &git.CloneOptions{
+ URL: g.Config.Url,
+ ReferenceName: ref,
+ })
+ g.CheckError(err, "clone source")
+ g.Debug("Cloned source (%s) in %v", tag, time.Since(now))
+}
+
+func (g *Global) ClonePages() {
+ ref := plumbing.NewBranchReferenceName("gh-pages")
+ ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
+ defer cancel()
+
+ now := time.Now()
+ repo, err := git.CloneContext(ctx, memory.NewStorage(), g.DstFs, &git.CloneOptions{
+ URL: g.Config.Url,
+ ReferenceName: ref,
+ RemoteName: "origin",
+ })
+ g.CheckError(err, "clone gh-pages")
+ g.Repo = repo
+ g.Debug("Cloned pages in %v", time.Since(now))
+}
+
+func (g *Global) DockerCmd(source string, output *strings.Builder) *exec.Cmd {
+ cmd := exec.Command("/usr/local/bin/docker", "run", "-i", "--rm", "asciidoctor/docker-asciidoctor")
+ cmd.Args = append(cmd.Args, "asciidoctor")
+ cmd.Args = append(cmd.Args, "-a", "linkcss")
+ cmd.Args = append(cmd.Args, "-a", "icons=font")
+ cmd.Args = append(cmd.Args, "-a", "nofooter")
+ cmd.Args = append(cmd.Args, "-a", "source-highlighter=pygments") // rouge is recommended, but our stylesheet is for pygments
+ cmd.Args = append(cmd.Args, "-s", "-o", "-", "-") // pipe it
+ cmd.Stdin = strings.NewReader(source)
+ cmd.Stdout = output
+ cmd.Stderr = &strings.Builder{}
+
+ return cmd
+}
+
+func (g *Global) ProcessManPage(page os.FileInfo) {
+ source := g.ReadFile(page.Name())
+ // Let's look for the description
+ inName := false
+ desc := ""
+ name := ""
+ title := ""
+ for _, line := range strings.Split(source, "\n") {
+ line = strings.TrimRight(line, " \t\r")
+ if line == "" {
+ continue
+ }
+ if strings.HasPrefix(line, "= ") {
+ title = strings.TrimSpace(line[2:])
+ continue
+ }
+ if line == "== NAME" {
+ inName = true
+ continue
+ }
+ if inName {
+ w := strings.SplitN(line, " - ", 2)
+ if len(w) != 2 || w[1] == "" {
+ g.Fatal("page %s NAME malformed", page.Name())
+ }
+ name = w[0]
+ desc = w[1]
+ strings.TrimSpace(name)
+ strings.TrimSpace(desc)
+ break
+ }
+ }
+
+ if desc == "" {
+ g.Fatal("page %s NAME missing", page.Name())
+ }
+ if title == "" {
+ g.Fatal("page %s title missing", page.Name())
+ }
+
+ html := &strings.Builder{}
+ w := strings.SplitN(title, "(", 2)
+ sect := strings.TrimSuffix(w[1], ")")
+ if len(w) != 2 || name != w[0] || !strings.HasSuffix(w[1], ")") {
+ g.Fatal("page %s title incorrect (%s)", page.Name(), name)
+ }
+ if page.Name() != name+"."+sect+".adoc" {
+ g.Fatal("page %s(%s) does not match file name %s", name, sect, page.Name())
+ }
+ _, _ = fmt.Fprintf(html, "---\n")
+ _, _ = fmt.Fprintf(html, "version: %s\n", g.Config.Version)
+ _, _ = fmt.Fprintf(html, "layout: %s\n", "manpage_v2")
+ _, _ = fmt.Fprintf(html, "title: %s\n", fmt.Sprintf("%s(%s)", name, sect))
+ _, _ = fmt.Fprintf(html, "---\n")
+ _, _ = fmt.Fprintf(html, "<h1>%s(%s)</h1>\n", name, sect)
+
+
+ cmd := g.DockerCmd(source, html)
+ err := cmd.Run()
+ if err != nil {
+ g.Fatal("Failed %v: %s", err, cmd.Stderr)
+ }
+
+ g.Pages[page.Name()] = &Page{
+ Name: name,
+ Section: sect,
+ Description: desc,
+ Content: html.String(),
+ }
+ g.Log.TRACE.Printf("HTML for %s:\n%s\n", name, html.String())
+}
+
+func (g *Global) ReadFile(name string) string {
+ f, err := g.SrcFs.Open(path.Join("docs/man", name))
+ g.CheckError(err, "open file %s", name)
+ b, err := ioutil.ReadAll(f)
+ g.CheckError(err, "read file %s", name)
+ return string(b)
+}
+
+func (g *Global) LoadSection(name string) {
+ section := strings.TrimPrefix(name, "man")
+
+ g.Sections[section] = &Section{
+ Name: section,
+ Synopsis: g.ReadFile(name + ".sect"),
+ Description: g.ReadFile(name + ".desc"),
+ }
+}
+
+func (g *Global) ProcessSource() {
+ pages, err := g.SrcFs.ReadDir("docs/man")
+ g.CheckError(err, "reading source directory")
+ count := 0
+ g.Debug("Total of %d files in man directory", len(pages))
+ now := time.Now()
+ for i, page := range pages {
+ if page.IsDir() {
+ continue
+ }
+ if strings.HasSuffix(page.Name(), ".sect") {
+ g.LoadSection(strings.TrimSuffix(page.Name(), ".sect"))
+ }
+ if !strings.HasSuffix(page.Name(), ".adoc") {
+ continue
+ }
+ g.ProcessManPage(page)
+ g.Print("Processed %s (%d of %d)", page.Name(), i, len(pages))
+ count++
+ }
+ g.Print("Processed %d pages in %v", count, time.Since(now))
+}
+
+func (g *Global) GenerateToC() {
+ toc := &strings.Builder{}
+ idx := &strings.Builder{}
+
+ for _, page := range g.Pages {
+ if sect := g.Sections[page.Section]; sect == nil {
+ g.Fatal("page %s section %s not found", page.Name, page.Section)
+ } else {
+ sect.Pages = append(sect.Pages, page)
+ }
+ }
+
+ var sects []string
+ for name, sect := range g.Sections {
+ sects = append(sects, name)
+ sort.Slice(sect.Pages, func(i, j int) bool { return sect.Pages[i].Name < sect.Pages[j].Name })
+ }
+ sort.Strings(sects)
+
+ // And also the index page.
+
+ // Emit the toc leader part
+ toc.WriteString("<nav id=\"toc\" class=\"toc2\">\n")
+ toc.WriteString("<div id=\"toctitle\">Table of Contents</div>\n")
+ toc.WriteString("<ul class=\"sectlevel1\n\">\n")
+
+ idx.WriteString("= NNG Reference Manual\n")
+
+ for _, sect := range sects {
+ s := g.Sections[sect]
+ _, _ = fmt.Fprintf(toc, "<li>%s</li>\n", s.Synopsis)
+ _, _ = fmt.Fprintf(toc, "<ul class=\"sectlevel2\">\n")
+
+ _, _ = fmt.Fprintf(idx, "\n== Section %s: %s\n\n", s.Name, s.Synopsis)
+ _, _ = fmt.Fprintln(idx, s.Description)
+ _, _ = fmt.Fprintln(idx, "\n[cols=\"3,5\"]\n|===")
+
+ for _, page := range s.Pages {
+ _, _ = fmt.Fprintf(toc, "<li><a href=\"%s.%s.html\">%s</a></li>\n",
+ page.Name, page.Section, page.Name)
+ _, _ = fmt.Fprintf(idx, "|xref:%s.%s.adoc[%s(%s)]\n", page.Name, page.Section,
+ page.Name, page.Section)
+ _, _ = fmt.Fprintf(idx, "|%s\n", page.Description)
+ }
+
+ _, _ = fmt.Fprintf(toc, "</ul>\n")
+ _, _ = fmt.Fprintln(idx, "|===")
+ }
+ _, _ = fmt.Fprintf(toc, "</ul>\n")
+ _, _ = fmt.Fprintf(toc, "</nav>\n")
+
+ index := &strings.Builder{}
+ _, _ = fmt.Fprintf(index, "---\n")
+ _, _ = fmt.Fprintf(index, "version: %s\n", g.Config.Version)
+ _, _ = fmt.Fprintf(index, "layout: %s\n", "manpage_v2")
+ _, _ = fmt.Fprintf(index, "---\n")
+ _, _ = fmt.Fprintf(index, "<h1>NNG Reference Manual</h1>\n")
+
+ cmd := g.DockerCmd(idx.String(), index)
+ cmd.Stdout = index
+ err := cmd.Run()
+ g.CheckError(err, "formatting index")
+ g.Index = index.String()
+ g.ToC = toc.String()
+}
+
+func (g *Global) CreateBranch() {
+ brName := uuid.New().String()
+ var err error
+
+ refName := plumbing.ReferenceName("refs/heads/" + brName)
+ g.Branch = brName
+
+ g.WorkTree, err = g.Repo.Worktree()
+ g.CheckError(err, "getting worktree")
+
+ err = g.WorkTree.Checkout(&git.CheckoutOptions{
+ Branch: refName,
+ Create: true,
+ })
+ g.CheckError(err, "creating branch")
+ g.Print("Checked out branch %v", brName)
+ pr, err := g.Repo.Head()
+ g.CheckError(err, "getting head hash")
+ g.OldHash = pr.Hash()
+}
+
+func (g *Global) WriteFile(name string, content string) {
+ full := path.Join(g.DstDir, name)
+ f, err := g.DstFs.Create(full)
+ g.CheckError(err, "creating file %s", name)
+ _, err = f.Write([]byte(content))
+ g.CheckError(err, "writing file %s", name)
+ err = f.Close()
+ g.CheckError(err, "closing file %s", name)
+ g.Add(name)
+}
+
+func (g *Global) Add(name string) {
+ g.Log.TRACE.Printf("Adding file %s", name)
+ g.Added[name] = true
+}
+
+func (g *Global) Delete(name string) {
+ g.Debug("Removing file %s", name)
+ _, err := g.WorkTree.Remove(path.Join(g.DstDir, name))
+ g.CheckError(err, "removing file %s", name)
+}
+
+func (g *Global) Commit() {
+ if status, err := g.WorkTree.Status(); status == nil {
+ g.CheckError(err, "obtaining status")
+ } else if status.IsClean() {
+ g.Print("No changes to commit.")
+ return
+ }
+ message := g.Config.Message
+ if message == "" {
+ message = "Manual page updates for " + g.Config.Version
+ }
+ var err error
+ g.NewHash, err = g.WorkTree.Commit(message, &git.CommitOptions{
+ Author: &object.Signature{
+ Email: g.Config.Email,
+ Name: g.Config.Author,
+ When: time.Now(),
+ },
+ })
+ g.CheckError(err, "committing branch")
+}
+
+func (g *Global) Push() {
+ if g.NewHash.IsZero() {
+ g.Print("Nothing to push.")
+ return
+ }
+
+ ci, err := g.Repo.Log(&git.LogOptions{
+ From: g.NewHash,
+ })
+ g.CheckError(err, "getting commit log")
+ commit, err := ci.Next()
+ g.CheckError(err, "getting single commit")
+ if commit != nil {
+ g.Print(commit.String())
+ if fs, _ := commit.Stats(); fs != nil {
+ g.Debug(fs.String())
+ }
+ }
+ if g.Config.DryRun {
+ g.Print("Not pushing changes (dry-run mode.)")
+ } else {
+ err := g.Repo.Push(&git.PushOptions{
+ RemoteName: "origin",
+ })
+ g.CheckError(err, "pushing changes")
+ g.Print("Pushed branch %v\n", g.Branch)
+ }
+}
+
+func (g *Global) WriteOutput() {
+
+ for _, p := range g.Pages {
+ fName := fmt.Sprintf("%s.%s.html", p.Name, p.Section)
+ g.WriteFile(fName, p.Content)
+ }
+ g.WriteFile("_toc.html", g.ToC)
+ g.WriteFile("index.html", g.Index)
+
+ _, err := g.WorkTree.Add(g.DstDir)
+ g.CheckError(err, "adding directory")
+ files, err := g.DstFs.ReadDir(g.DstDir)
+ g.CheckError(err, "scanning destination directory")
+ for _, file := range files {
+ if file.IsDir() {
+ continue
+ }
+ if g.Added[file.Name()] {
+ continue
+ }
+ g.Delete(file.Name())
+ }
+ status, err := g.WorkTree.Status()
+ g.CheckError(err, "obtaining commit status")
+ if !status.IsClean() {
+ g.Debug("No changes.")
+ } else {
+ g.Debug(status.String())
+ }
+}
+
+func main() {
+ g := &Global{}
+ flag.Parse()
+ g.Init()
+ defer g.Destroy()
+
+ g.CloneSource()
+ g.ClonePages()
+ g.ProcessSource()
+ g.GenerateToC()
+ g.CreateBranch()
+ g.WriteOutput()
+ g.Commit()
+ g.Push()
+}