Advertisement
Guest User

main.go

a guest
Jul 30th, 2020
75
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Go 13.45 KB | None | 0 0
  1. // Package godoc-static generates static Go documentation
  2. package main
  3.  
  4. import (
  5.     "archive/zip"
  6.     "bytes"
  7.     "errors"
  8.     "flag"
  9.     "fmt"
  10.     "io/ioutil"
  11.     "log"
  12.     "net/http"
  13.     "os"
  14.     "os/exec"
  15.     "os/signal"
  16.     "path"
  17.     "path/filepath"
  18.     "sort"
  19.     "strings"
  20.     "time"
  21.  
  22.     "github.com/PuerkitoBio/goquery"
  23.     "github.com/yuin/goldmark"
  24.     "github.com/yuin/goldmark/extension"
  25.     gmhtml "github.com/yuin/goldmark/renderer/html"
  26.     "golang.org/x/net/html"
  27. )
  28.  
  29. var (
  30.     listenAddress       string
  31.     siteName            string
  32.     siteDescription     string
  33.     siteDescriptionFile string
  34.     siteFooter          string
  35.     siteFooterFile      string
  36.     siteDestination     string
  37.     siteZip             string
  38.     linkIndex           bool
  39.     excludePackages     string
  40.     verbose             bool
  41.  
  42.     godoc  *exec.Cmd
  43.     outZip *zip.Writer
  44. )
  45.  
  46. func main() {
  47.     log.SetPrefix("")
  48.     log.SetFlags(0)
  49.  
  50.     flag.StringVar(&listenAddress, "listen-address", "localhost:9001", "address for godoc to listen on while scraping pages")
  51.     flag.StringVar(&siteName, "site-name", "Documentation", "site name")
  52.     flag.StringVar(&siteDescription, "site-description", "", "site description (markdown-enabled)")
  53.     flag.StringVar(&siteDescriptionFile, "site-description-file", "", "path to markdown file containing site description")
  54.     flag.StringVar(&siteFooter, "site-footer", "", "site footer (markdown-enabled)")
  55.     flag.StringVar(&siteFooterFile, "site-footer-file", "", "path to markdown file containing site footer")
  56.     flag.StringVar(&siteDestination, "destination", "", "path to write site HTML")
  57.     flag.StringVar(&siteZip, "zip", "docs.zip", "name of site ZIP file (blank to disable)")
  58.     flag.BoolVar(&linkIndex, "link-index", false, "set link targets to index.html instead of folder")
  59.     flag.StringVar(&excludePackages, "exclude", "", "list of packages to exclude from index")
  60.     flag.BoolVar(&verbose, "verbose", false, "enable verbose logging")
  61.     flag.Parse()
  62.  
  63.     err := run()
  64.     if godoc != nil {
  65.         godoc.Process.Kill()
  66.     }
  67.     if err != nil {
  68.         log.Fatal(err)
  69.     }
  70. }
  71.  
  72. func filterPkgsWithExcludes(pkgs []string) []string {
  73.     excludePackagesSplit := strings.Split(excludePackages, " ")
  74.     var tmpPkgs []string
  75. PACKAGEINDEX:
  76.     for _, pkg := range pkgs {
  77.         for _, excludePackage := range excludePackagesSplit {
  78.             if strings.Contains(pkg, "\\") || strings.Contains(pkg, "testdata") || strings.Contains(pkg, "internal") || pkg == "cmd" || pkg == excludePackage || strings.HasPrefix(pkg, excludePackage+"/") {
  79.                 continue PACKAGEINDEX
  80.             }
  81.         }
  82.         tmpPkgs = append(tmpPkgs, pkg)
  83.     }
  84.     return tmpPkgs
  85. }
  86.  
  87. func getTmpDir() string {
  88.     tmpDir := os.TempDir()
  89.     if _, err := os.Stat(tmpDir); os.IsNotExist(err) {
  90.         mkDirErr := os.MkdirAll(tmpDir, 0755)
  91.         if _, err = os.Stat(tmpDir); os.IsNotExist(err) {
  92.             log.Fatalf("failed to create missing temporary directory %s: %s", tmpDir, mkDirErr)
  93.         }
  94.     }
  95.     return tmpDir
  96. }
  97.  
  98. func writeFile(buf *bytes.Buffer, fileDir string, fileName string) error {
  99.     if outZip != nil {
  100.         fn := fileDir
  101.         if fn != "" {
  102.             fn += "/"
  103.         }
  104.         fn += fileName
  105.  
  106.         outZipFile, err := outZip.Create(fn)
  107.         if err != nil {
  108.             return fmt.Errorf("failed to create zip file %s: %s", fn, err)
  109.         }
  110.  
  111.         _, err = outZipFile.Write(buf.Bytes())
  112.         if err != nil {
  113.             return fmt.Errorf("failed to write zip file %s: %s", fn, err)
  114.         }
  115.     }
  116.  
  117.     return ioutil.WriteFile(path.Join(siteDestination, fileDir, fileName), buf.Bytes(), 0755)
  118. }
  119.  
  120. func run() error {
  121.     var buf bytes.Buffer
  122.     timeStarted := time.Now()
  123.  
  124.     if siteDestination == "" {
  125.         return errors.New("--destination must be set")
  126.     }
  127.  
  128.     if siteDescriptionFile != "" {
  129.         siteDescriptionBytes, err := ioutil.ReadFile(siteDescriptionFile)
  130.         if err != nil {
  131.             return fmt.Errorf("failed to read site description file %s: %s", siteDescriptionFile, err)
  132.         }
  133.         siteDescription = string(siteDescriptionBytes)
  134.     }
  135.  
  136.     if siteDescription != "" {
  137.         markdown := goldmark.New(
  138.             goldmark.WithRendererOptions(
  139.                 gmhtml.WithUnsafe(),
  140.             ),
  141.             goldmark.WithExtensions(
  142.                 extension.NewLinkify(),
  143.             ),
  144.         )
  145.  
  146.         buf.Reset()
  147.         err := markdown.Convert([]byte(siteDescription), &buf)
  148.         if err != nil {
  149.             return fmt.Errorf("failed to render site description markdown: %s", err)
  150.         }
  151.         siteDescription = buf.String()
  152.     }
  153.  
  154.     if siteFooterFile != "" {
  155.         siteFooterBytes, err := ioutil.ReadFile(siteFooterFile)
  156.         if err != nil {
  157.             return fmt.Errorf("failed to read site footer file %s: %s", siteFooterFile, err)
  158.         }
  159.         siteFooter = string(siteFooterBytes)
  160.     }
  161.  
  162.     if siteFooter != "" {
  163.         markdown := goldmark.New(
  164.             goldmark.WithRendererOptions(
  165.                 gmhtml.WithUnsafe(),
  166.             ),
  167.             goldmark.WithExtensions(
  168.                 extension.NewLinkify(),
  169.             ),
  170.         )
  171.  
  172.         buf.Reset()
  173.         err := markdown.Convert([]byte(siteFooter), &buf)
  174.         if err != nil {
  175.             return fmt.Errorf("failed to render site footer markdown: %s", err)
  176.         }
  177.         siteFooter = buf.String()
  178.     }
  179.  
  180.     if siteZip != "" {
  181.         outZipFile, err := os.Create(filepath.Join(siteDestination, siteZip))
  182.         if err != nil {
  183.             return fmt.Errorf("failed to create zip file %s: %s", filepath.Join(siteDestination, siteZip), err)
  184.         }
  185.         defer outZipFile.Close()
  186.  
  187.         outZip = zip.NewWriter(outZipFile)
  188.         defer outZip.Close()
  189.     }
  190.  
  191.     if verbose {
  192.         log.Println("Starting godoc...")
  193.     }
  194.  
  195.     godoc = exec.Command("godoc", fmt.Sprintf("-http=%s", listenAddress))
  196.     godoc.Stdin = os.Stdin
  197.     godoc.Stdout = os.Stdout
  198.     godoc.Stderr = os.Stderr
  199.     setDeathSignal(godoc)
  200.  
  201.     err := godoc.Start()
  202.     if err != nil {
  203.         return fmt.Errorf("failed to execute godoc: %s", err)
  204.     }
  205.  
  206.     c := make(chan os.Signal, 1)
  207.     signal.Notify(c, os.Interrupt)
  208.     go func() {
  209.         <-c
  210.         godoc.Process.Kill()
  211.         os.Exit(1)
  212.     }()
  213.  
  214.     godocStarted := time.Now()
  215.  
  216.     pkgs := flag.Args()
  217.  
  218.     if len(pkgs) == 0 || (len(pkgs) == 1 && pkgs[0] == "") {
  219.         buf.Reset()
  220.  
  221.         cmd := exec.Command("go", "list", "...")
  222.         cmd.Dir = os.TempDir()
  223.         cmd.Stdout = &buf
  224.         setDeathSignal(cmd)
  225.  
  226.         err = cmd.Run()
  227.         if err != nil {
  228.             return fmt.Errorf("failed to list system packages: %s", err)
  229.         }
  230.  
  231.         pkgs = strings.Split(strings.TrimSpace(buf.String()), "\n")
  232.     }
  233.  
  234.     var newPkgs []string
  235.     for _, pkg := range pkgs {
  236.         if strings.TrimSpace(pkg) == "" {
  237.             continue
  238.         }
  239.  
  240.         buf.Reset()
  241.  
  242.         newPkgs = append(newPkgs, pkg)
  243.  
  244.         cmd := exec.Command("go", "list", "-find", "-f", `{{ .Dir }}`, pkg)
  245.         cmd.Dir = os.TempDir()
  246.         cmd.Stdout = &buf
  247.         setDeathSignal(cmd)
  248.  
  249.         err = cmd.Run()
  250.         if err != nil {
  251.             return fmt.Errorf("failed to list source directory of package %s: %s", pkg, err)
  252.         }
  253.  
  254.         pkgPath := strings.TrimSpace(buf.String())
  255.         if pkgPath != "" {
  256.             err := filepath.Walk(pkgPath, func(path string, info os.FileInfo, err error) error {
  257.                 if err != nil {
  258.                     return err
  259.                 } else if !info.IsDir() {
  260.                     return nil
  261.                 } else if strings.HasPrefix(filepath.Base(path), ".") {
  262.                     return filepath.SkipDir
  263.                 }
  264.  
  265.                 if len(path) > len(pkgPath) && strings.HasPrefix(path, pkgPath) {
  266.                     newPkgs = append(newPkgs, pkg+path[len(pkgPath):])
  267.                 }
  268.                 return nil
  269.             })
  270.             if err != nil {
  271.                 return fmt.Errorf("failed to walk source directory of package %s: %s", pkg, err)
  272.             }
  273.         }
  274.         buf.Reset()
  275.     }
  276.     pkgs = uniqueStrings(newPkgs)
  277.  
  278.     if len(pkgs) == 0 {
  279.         return errors.New("failed to generate docs: provide the name of at least one package to generate documentation for")
  280.     }
  281.  
  282.     filterPkgs := pkgs
  283.  
  284.     for _, pkg := range pkgs {
  285.         subPkgs := strings.Split(pkg, "/")
  286.         for i := range subPkgs {
  287.             pkgs = append(pkgs, strings.Join(subPkgs[0:i+1], "/"))
  288.         }
  289.     }
  290.     pkgs = uniqueStrings(pkgs)
  291.  
  292.     pkgs = filterPkgsWithExcludes(pkgs)
  293.  
  294.     sort.Slice(pkgs, func(i, j int) bool {
  295.         return strings.ToLower(pkgs[i]) < strings.ToLower(pkgs[j])
  296.     })
  297.  
  298.     // Allow some time for godoc to initialize
  299.  
  300.     if time.Since(godocStarted) < 3*time.Second {
  301.         time.Sleep((3 * time.Second) - time.Since(godocStarted))
  302.     }
  303.  
  304.     done := make(chan error)
  305.     timeout := time.After(15 * time.Second)
  306.  
  307.     go func() {
  308.         var (
  309.             res *http.Response
  310.             err error
  311.         )
  312.         for _, pkg := range pkgs {
  313.             if verbose {
  314.                 log.Printf("Copying %s docs...", pkg)
  315.             }
  316.  
  317.             // Rely on timeout to break loop
  318.             for {
  319.                 res, err = http.Get(fmt.Sprintf("http://%s/pkg/%s/", listenAddress, pkg))
  320.                 if err == nil {
  321.                     break
  322.                 }
  323.             }
  324.  
  325.             // Load the HTML document
  326.             doc, err := goquery.NewDocumentFromReader(res.Body)
  327.             if err != nil {
  328.                 done <- fmt.Errorf("failed to get page of %s: %s", pkg, err)
  329.                 return
  330.             }
  331.  
  332.             doc.Find("title").First().SetHtml(fmt.Sprintf("%s - %s", path.Base(pkg), siteName))
  333.  
  334.             updatePage(doc, relativeBasePath(pkg), siteName)
  335.  
  336.             localPkgPath := path.Join(siteDestination, pkg)
  337.  
  338.             err = os.MkdirAll(localPkgPath, 0755)
  339.             if err != nil {
  340.                 done <- fmt.Errorf("failed to make directory %s: %s", localPkgPath, err)
  341.                 return
  342.             }
  343.  
  344.             buf.Reset()
  345.             err = html.Render(&buf, doc.Nodes[0])
  346.             if err != nil {
  347.                 done <- fmt.Errorf("failed to render HTML: %s", err)
  348.                 return
  349.             }
  350.             err = writeFile(&buf, pkg, "index.html")
  351.             if err != nil {
  352.                 done <- fmt.Errorf("failed to write docs for %s: %s", pkg, err)
  353.                 return
  354.             }
  355.         }
  356.         done <- nil
  357.     }()
  358.  
  359.     select {
  360.     case <-timeout:
  361.         return errors.New("godoc failed to start in time")
  362.     case err = <-done:
  363.         if err != nil {
  364.             return fmt.Errorf("failed to copy docs: %s", err)
  365.         }
  366.     }
  367.  
  368.     // Write source files
  369.  
  370.     err = os.MkdirAll(path.Join(siteDestination, "src"), 0755)
  371.     if err != nil {
  372.         return fmt.Errorf("failed to make directory lib: %s", err)
  373.     }
  374.  
  375.     filterPkgs = filterPkgsWithExcludes(filterPkgs)
  376.  
  377.     for _, pkg := range filterPkgs {
  378.         if verbose {
  379.             log.Printf("Copying %s sources...", pkg)
  380.         }
  381.  
  382.         buf.Reset()
  383.  
  384.         cmd := exec.Command("go", "list", "-find", "-f",
  385.             `{{ join .GoFiles "\n" }}`+"\n"+
  386.                 `{{ join .CgoFiles "\n" }}`+"\n"+
  387.                 `{{ join .CFiles "\n" }}`+"\n"+
  388.                 `{{ join .CXXFiles "\n" }}`+"\n"+
  389.                 `{{ join .MFiles "\n" }}`+"\n"+
  390.                 `{{ join .HFiles "\n" }}`+"\n"+
  391.                 `{{ join .FFiles "\n" }}`+"\n"+
  392.                 `{{ join .SFiles "\n" }}`+"\n"+
  393.                 `{{ join .SwigFiles "\n" }}`+"\n"+
  394.                 `{{ join .SwigCXXFiles "\n" }}`+"\n"+
  395.                 `{{ join .TestGoFiles "\n" }}`+"\n"+
  396.                 `{{ join .XTestGoFiles "\n" }}`,
  397.             pkg)
  398.         cmd.Dir = getTmpDir()
  399.         cmd.Stdout = &buf
  400.         setDeathSignal(cmd)
  401.  
  402.         err = cmd.Run()
  403.         if err != nil {
  404.             //return fmt.Errorf("failed to list source files of package %s: %s", pkg, err)
  405.             continue // This is expected for packages without source files
  406.         }
  407.  
  408.         sourceFiles := append(strings.Split(buf.String(), "\n"), "index.html")
  409.        
  410.         for _, sourceFile := range sourceFiles {
  411.             sourceFile = strings.TrimSpace(sourceFile)
  412.             if sourceFile == "" {
  413.                 continue
  414.             }
  415.  
  416.             // Rely on timeout to break loop
  417.             res, err := http.Get(fmt.Sprintf("http://%s/src/%s/%s", listenAddress, pkg, sourceFile))
  418.             if err != nil {
  419.                 return fmt.Errorf("failed to get source file page %s for package %s: %s", sourceFile, pkg, err)
  420.             }
  421.  
  422.             // Load the HTML document
  423.             doc, err := goquery.NewDocumentFromReader(res.Body)
  424.             if err != nil {
  425.                 return fmt.Errorf("failed to load document from page for package %s: %s", pkg, err)
  426.             }
  427.  
  428.             doc.Find("title").First().SetHtml(fmt.Sprintf("%s - %s", path.Base(pkg), siteName))
  429.  
  430.             updatePage(doc, relativeBasePath("src/"+pkg), siteName)
  431.  
  432.             doc.Find(".layout").First().Find("a").Each(func(_ int, selection *goquery.Selection) {
  433.                 href := selection.AttrOr("href", "")
  434.                 if !strings.HasSuffix(href, ".") && !strings.HasSuffix(href, "/") && !strings.HasSuffix(href, ".html") {
  435.                     selection.SetAttr("href", href+".html")
  436.                 }
  437.             })
  438.  
  439.             pkgSrcPath := path.Join(siteDestination, "src", pkg)
  440.  
  441.             err = os.MkdirAll(pkgSrcPath, 0755)
  442.             if err != nil {
  443.                 return fmt.Errorf("failed to make directory %s: %s", pkgSrcPath, err)
  444.             }
  445.  
  446.             buf.Reset()
  447.             err = html.Render(&buf, doc.Nodes[0])
  448.             if err != nil {
  449.                 return fmt.Errorf("failed to render HTML: %s", err)
  450.             }
  451.  
  452.             outFileName := sourceFile
  453.             if !strings.HasSuffix(outFileName, ".html") {
  454.                 outFileName += ".html"
  455.             }
  456.             err = writeFile(&buf, "src/"+pkg, outFileName)
  457.             if err != nil {
  458.                 return fmt.Errorf("failed to write docs for %s: %s", pkg, err)
  459.             }
  460.         }
  461.     }
  462.  
  463.     // Write style.css
  464.  
  465.     if verbose {
  466.         log.Println("Copying style.css...")
  467.     }
  468.  
  469.     err = os.MkdirAll(path.Join(siteDestination, "lib"), 0755)
  470.     if err != nil {
  471.         return fmt.Errorf("failed to make directory lib: %s", err)
  472.     }
  473.  
  474.     res, err := http.Get(fmt.Sprintf("http://%s/lib/godoc/style.css", listenAddress))
  475.     if err != nil {
  476.         return fmt.Errorf("failed to get syle.css: %s", err)
  477.     }
  478.  
  479.     buf.Reset()
  480.  
  481.     _, err = buf.ReadFrom(res.Body)
  482.     res.Body.Close()
  483.     if err != nil {
  484.         return fmt.Errorf("failed to get style.css: %s", err)
  485.     }
  486.  
  487.     buf.WriteString("\n" + additionalCSS)
  488.  
  489.     err = writeFile(&buf, "lib", "style.css")
  490.     if err != nil {
  491.         return fmt.Errorf("failed to write style.css: %s", err)
  492.     }
  493.  
  494.     // Write index
  495.  
  496.     if verbose {
  497.         log.Println("Writing index...")
  498.     }
  499.  
  500.     err = writeIndex(&buf, pkgs, filterPkgs)
  501.     if err != nil {
  502.         return fmt.Errorf("failed to write index: %s", err)
  503.     }
  504.  
  505.     if verbose {
  506.         log.Printf("Generated documentation in %s.", time.Since(timeStarted).Round(time.Second))
  507.     }
  508.     return nil
  509. }
  510.  
  511. func relativeBasePath(p string) string {
  512.     var r string
  513.     if p != "" {
  514.         r += "../"
  515.     }
  516.     p = filepath.ToSlash(p)
  517.     for i := strings.Count(p, "/"); i > 0; i-- {
  518.         r += "../"
  519.     }
  520.     return r
  521. }
  522.  
  523. func uniqueStrings(strSlice []string) []string {
  524.     keys := make(map[string]bool)
  525.     var unique []string
  526.     for _, entry := range strSlice {
  527.         if _, value := keys[entry]; !value {
  528.             keys[entry] = true
  529.             unique = append(unique, entry)
  530.         }
  531.     }
  532.     return unique
  533. }
  534.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement