2021-12-22 08:14:34 -05:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
2023-02-14 14:06:46 -05:00
|
|
|
"errors"
|
2021-12-22 08:14:34 -05:00
|
|
|
"fmt"
|
2023-04-03 07:16:02 -04:00
|
|
|
cp "github.com/otiai10/copy"
|
2021-12-22 08:14:34 -05:00
|
|
|
"os"
|
2023-02-03 20:39:51 -05:00
|
|
|
"path/filepath"
|
2021-12-25 09:32:20 -05:00
|
|
|
"strings"
|
2021-12-22 08:14:34 -05:00
|
|
|
"text/template"
|
|
|
|
|
|
|
|
"github.com/PuerkitoBio/goquery"
|
2022-08-30 17:52:53 -04:00
|
|
|
"github.com/avelino/awesome-go/pkg/slug"
|
2021-12-22 08:14:34 -05:00
|
|
|
)
|
|
|
|
|
|
|
|
type Link struct {
|
|
|
|
Title string
|
|
|
|
Url string
|
|
|
|
Description string
|
|
|
|
}
|
|
|
|
|
2023-02-14 18:19:40 -05:00
|
|
|
// FIXME: rename to Category
|
2023-02-14 18:48:07 -05:00
|
|
|
type Category struct {
|
2021-12-22 08:14:34 -05:00
|
|
|
Title string
|
|
|
|
Slug string
|
|
|
|
Description string
|
|
|
|
Items []Link
|
|
|
|
}
|
|
|
|
|
2023-02-14 12:58:36 -05:00
|
|
|
// Source files
|
2023-02-03 20:39:51 -05:00
|
|
|
const readmePath = "README.md"
|
2023-02-14 12:49:59 -05:00
|
|
|
|
2023-02-14 12:58:36 -05:00
|
|
|
// This files should be copied 'as is' to outDir directory
|
2023-02-14 12:49:59 -05:00
|
|
|
var staticFiles = []string{
|
|
|
|
"tmpl/assets",
|
|
|
|
"tmpl/_redirects",
|
|
|
|
"tmpl/robots.txt",
|
|
|
|
}
|
2023-02-03 20:39:51 -05:00
|
|
|
|
2023-02-14 13:12:57 -05:00
|
|
|
// TODO: embed
|
|
|
|
// Templates
|
|
|
|
var tplIndex = template.Must(template.ParseFiles("tmpl/tmpl.html"))
|
|
|
|
var tplCategoryIndex = template.Must(template.ParseFiles("tmpl/cat-tmpl.html"))
|
|
|
|
var tplSitemap = template.Must(template.ParseFiles("tmpl/sitemap-tmpl.xml"))
|
2023-02-03 20:39:51 -05:00
|
|
|
|
2023-02-14 12:58:36 -05:00
|
|
|
// Output files
|
|
|
|
const outDir = "out/" // NOTE: trailing slash is required
|
2023-04-03 07:16:02 -04:00
|
|
|
|
|
|
|
var outIndexFile = filepath.Join(outDir, "index.html")
|
|
|
|
var outSitemapFile = filepath.Join(outDir, "sitemap.xml")
|
2023-02-03 20:39:51 -05:00
|
|
|
|
2021-12-22 08:14:34 -05:00
|
|
|
func main() {
|
2023-02-14 17:10:50 -05:00
|
|
|
if err := renderAll(); err != nil {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: choose a better name
|
|
|
|
func renderAll() error {
|
2023-02-14 18:39:11 -05:00
|
|
|
if err := dropCreateDir(outDir); err != nil {
|
|
|
|
return fmt.Errorf("unable to drop-create out dir: %w", err)
|
2023-02-03 21:01:22 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 18:39:17 -05:00
|
|
|
if err := renderIndex(readmePath, outIndexFile); err != nil {
|
2023-02-14 17:10:50 -05:00
|
|
|
return fmt.Errorf("unable to convert markdown to html: %w", err)
|
2022-08-30 09:20:58 -04:00
|
|
|
}
|
2023-02-03 20:39:51 -05:00
|
|
|
|
2023-04-03 07:16:02 -04:00
|
|
|
input, err := os.ReadFile(outIndexFile)
|
2021-12-22 08:14:34 -05:00
|
|
|
if err != nil {
|
2023-02-14 17:10:50 -05:00
|
|
|
return fmt.Errorf("unable to read converted html: %w", err)
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
2023-02-03 20:39:51 -05:00
|
|
|
|
2023-02-14 18:21:17 -05:00
|
|
|
doc, err := goquery.NewDocumentFromReader(bytes.NewReader(input))
|
2021-12-22 08:14:34 -05:00
|
|
|
if err != nil {
|
2023-02-14 17:10:50 -05:00
|
|
|
return fmt.Errorf("unable to create goquery instance: %w", err)
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
categories, err := extractCategories(doc)
|
2023-02-14 18:42:19 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to extract categories: %w", err)
|
|
|
|
}
|
2021-12-22 08:14:34 -05:00
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
if err := renderCategories(categories); err != nil {
|
2023-02-14 17:10:50 -05:00
|
|
|
return fmt.Errorf("unable to render categories: %w", err)
|
2023-02-03 20:39:51 -05:00
|
|
|
}
|
2023-02-14 17:26:55 -05:00
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
if err := rewriteLinksInIndex(doc, categories); err != nil {
|
2023-02-14 17:26:55 -05:00
|
|
|
return fmt.Errorf("unable to rewrite links in index: %w", err)
|
|
|
|
}
|
2022-08-30 09:20:58 -04:00
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
if err := renderSitemap(categories); err != nil {
|
2023-02-14 18:05:31 -05:00
|
|
|
return fmt.Errorf("unable to render sitemap: %w", err)
|
|
|
|
}
|
2023-04-03 07:16:02 -04:00
|
|
|
|
2023-02-14 12:49:59 -05:00
|
|
|
for _, srcFilename := range staticFiles {
|
|
|
|
dstFilename := filepath.Join(outDir, filepath.Base(srcFilename))
|
|
|
|
fmt.Printf("Copy static file: %s -> %s\n", srcFilename, dstFilename)
|
|
|
|
if err := cp.Copy(srcFilename, dstFilename); err != nil {
|
2023-02-14 17:10:50 -05:00
|
|
|
return fmt.Errorf("unable to copy static file `%s` to `%s`: %w", srcFilename, dstFilename, err)
|
2023-02-14 12:49:59 -05:00
|
|
|
}
|
2023-04-03 07:16:02 -04:00
|
|
|
}
|
2023-02-14 17:10:50 -05:00
|
|
|
|
|
|
|
return nil
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 18:39:11 -05:00
|
|
|
// dropCreateDir drop and create output directory
|
|
|
|
func dropCreateDir(dir string) error {
|
|
|
|
if err := os.RemoveAll(dir); err != nil {
|
|
|
|
return fmt.Errorf("unable to remove dir: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := mkdirAll(dir); err != nil {
|
|
|
|
return fmt.Errorf("unable to create dir: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
func extractCategories(doc *goquery.Document) (map[string]Category, error) {
|
|
|
|
categories := make(map[string]Category)
|
2023-02-14 18:42:19 -05:00
|
|
|
doc.
|
|
|
|
Find("body #contents").
|
|
|
|
NextFiltered("ul").
|
|
|
|
Find("ul").
|
|
|
|
Each(func(_ int, selUl *goquery.Selection) {
|
|
|
|
selUl.
|
|
|
|
Find("li a").
|
|
|
|
Each(func(_ int, s *goquery.Selection) {
|
|
|
|
selector, exists := s.Attr("href")
|
|
|
|
if !exists {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
category, err := makeCategoryByID(selector, doc)
|
2023-02-14 18:42:19 -05:00
|
|
|
if err != nil {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
categories[selector] = *category
|
2023-02-14 18:42:19 -05:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
|
|
|
// FIXME: handle error
|
2023-02-14 18:48:07 -05:00
|
|
|
return categories, nil
|
2023-02-14 18:42:19 -05:00
|
|
|
}
|
|
|
|
|
2023-02-03 20:39:51 -05:00
|
|
|
func mkdirAll(path string) error {
|
|
|
|
_, err := os.Stat(path)
|
|
|
|
// NOTE: directory is exists
|
|
|
|
if err == nil {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: unknown error
|
|
|
|
if !os.IsNotExist(err) {
|
2023-02-14 18:31:31 -05:00
|
|
|
return fmt.Errorf("unexpected result of dir stat: %w", err)
|
2023-02-03 20:39:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// NOTE: directory is not exists
|
2023-02-14 18:31:31 -05:00
|
|
|
if err := os.MkdirAll(path, 0755); err != nil {
|
|
|
|
return fmt.Errorf("unable to midirAll: %w", err)
|
2023-02-03 20:39:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
func renderCategories(categories map[string]Category) error {
|
|
|
|
for _, category := range categories {
|
|
|
|
categoryDir := filepath.Join(outDir, category.Slug)
|
2023-04-03 07:16:02 -04:00
|
|
|
if err := mkdirAll(categoryDir); err != nil {
|
2023-02-14 18:31:31 -05:00
|
|
|
return fmt.Errorf("unable to create category dir `%s`: %w", categoryDir, err)
|
2023-02-03 20:39:51 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: embed templates
|
|
|
|
// FIXME: parse templates once at start
|
2023-02-14 12:50:14 -05:00
|
|
|
categoryIndexFilename := filepath.Join(categoryDir, "index.html")
|
2023-02-14 17:58:11 -05:00
|
|
|
fmt.Printf("Write category Index file: %s\n", categoryIndexFilename)
|
|
|
|
|
|
|
|
buf := bytes.NewBuffer(nil)
|
2023-02-14 18:48:07 -05:00
|
|
|
if err := tplCategoryIndex.Execute(buf, category); err != nil {
|
2023-02-14 18:31:31 -05:00
|
|
|
return fmt.Errorf("unable to render category `%s`: %w", categoryDir, err)
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 17:58:11 -05:00
|
|
|
// Sanitize HTML. This is not necessary, but allows to have content
|
|
|
|
// of all html files in same style.
|
|
|
|
{
|
|
|
|
query, err := goquery.NewDocumentFromReader(buf)
|
|
|
|
if err != nil {
|
2023-02-14 18:31:31 -05:00
|
|
|
// FIXME: remove `unable to` from all fmt.Errorf
|
|
|
|
return fmt.Errorf("unable to create goquery instance for `%s`: %w", categoryDir, err)
|
2023-02-14 17:58:11 -05:00
|
|
|
}
|
2023-02-14 12:50:14 -05:00
|
|
|
|
2023-02-14 17:58:11 -05:00
|
|
|
html, err := query.Html()
|
|
|
|
if err != nil {
|
2023-02-14 18:31:31 -05:00
|
|
|
return fmt.Errorf("unable to render goquery html for `%s`: %w", categoryDir, err)
|
2023-02-14 17:58:11 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.WriteFile(categoryIndexFilename, []byte(html), 0644); err != nil {
|
2023-02-14 18:31:31 -05:00
|
|
|
return fmt.Errorf("unable to write category file `%s`: %w", categoryDir, err)
|
2023-02-14 17:58:11 -05:00
|
|
|
}
|
2023-02-03 20:39:51 -05:00
|
|
|
}
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
2023-02-03 20:39:51 -05:00
|
|
|
|
|
|
|
return nil
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
func renderSitemap(categories map[string]Category) error {
|
2023-02-14 12:50:14 -05:00
|
|
|
// FIXME: handle error
|
2023-02-14 18:05:31 -05:00
|
|
|
f, err := os.Create(outSitemapFile)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to create sitemap file `%s`: %w", outSitemapFile, err)
|
|
|
|
}
|
|
|
|
|
2023-02-14 12:50:14 -05:00
|
|
|
fmt.Printf("Render Sitemap to: %s\n", outSitemapFile)
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
if err := tplSitemap.Execute(f, categories); err != nil {
|
2023-02-14 18:05:31 -05:00
|
|
|
return fmt.Errorf("unable to render sitemap: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
func makeCategoryByID(selector string, doc *goquery.Document) (*Category, error) {
|
|
|
|
var category Category
|
2023-02-14 14:06:46 -05:00
|
|
|
var err error
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
doc.Find(selector).EachWithBreak(func(_ int, selCatHeader *goquery.Selection) bool {
|
2023-02-14 14:24:30 -05:00
|
|
|
selDescr := selCatHeader.NextFiltered("p")
|
|
|
|
// FIXME: bug. this would select links from all neighboring
|
|
|
|
// sub-categories until the next category. To prevent this we should
|
|
|
|
// find only first ul
|
|
|
|
ul := selCatHeader.NextFilteredUntil("ul", "h2")
|
2021-12-22 08:14:34 -05:00
|
|
|
|
2023-02-14 14:06:46 -05:00
|
|
|
var links []Link
|
2023-02-14 14:24:30 -05:00
|
|
|
ul.Find("li").Each(func(_ int, selLi *goquery.Selection) {
|
|
|
|
selLink := selLi.Find("a")
|
|
|
|
url, _ := selLink.Attr("href")
|
2021-12-22 08:14:34 -05:00
|
|
|
link := Link{
|
2023-02-14 14:24:30 -05:00
|
|
|
Title: selLink.Text(),
|
|
|
|
// FIXME: Title contains only title but description contains Title + description
|
|
|
|
Description: selLi.Text(),
|
2021-12-22 08:14:34 -05:00
|
|
|
Url: url,
|
|
|
|
}
|
|
|
|
links = append(links, link)
|
|
|
|
})
|
2023-02-14 14:06:46 -05:00
|
|
|
// FIXME: In this case we would have an empty category in main index.html with link to 404 page.
|
2022-08-30 10:21:44 -04:00
|
|
|
if len(links) == 0 {
|
2023-02-14 18:48:07 -05:00
|
|
|
err = errors.New("category does not contain links")
|
|
|
|
return false
|
2022-08-30 10:21:44 -04:00
|
|
|
}
|
2023-02-14 18:48:07 -05:00
|
|
|
|
|
|
|
category = Category{
|
2023-02-14 14:24:30 -05:00
|
|
|
Slug: slug.Generate(selCatHeader.Text()),
|
|
|
|
Title: selCatHeader.Text(),
|
|
|
|
Description: selDescr.Text(),
|
2021-12-22 08:14:34 -05:00
|
|
|
Items: links,
|
|
|
|
}
|
2023-02-14 18:48:07 -05:00
|
|
|
|
|
|
|
return true
|
2021-12-22 08:14:34 -05:00
|
|
|
})
|
2023-02-14 14:06:46 -05:00
|
|
|
|
|
|
|
if err != nil {
|
2023-02-14 18:48:07 -05:00
|
|
|
return nil, fmt.Errorf("unable to build a category: %w", err)
|
2023-02-14 14:06:46 -05:00
|
|
|
}
|
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
return &category, nil
|
2021-12-22 08:14:34 -05:00
|
|
|
}
|
2021-12-25 09:32:20 -05:00
|
|
|
|
2023-02-14 18:48:07 -05:00
|
|
|
func rewriteLinksInIndex(doc *goquery.Document, categories map[string]Category) error {
|
2023-02-14 18:21:17 -05:00
|
|
|
doc.
|
|
|
|
Find("body #content ul li ul li a").
|
|
|
|
Each(func(_ int, s *goquery.Selection) {
|
|
|
|
href, hrefExists := s.Attr("href")
|
|
|
|
if !hrefExists {
|
|
|
|
// FIXME: looks like is an error. Tag `a` in our case always
|
|
|
|
// should have `href` attr.
|
|
|
|
return
|
|
|
|
}
|
2021-12-25 09:32:20 -05:00
|
|
|
|
2023-02-14 18:21:17 -05:00
|
|
|
// do not replace links if no page has been created for it
|
2023-02-14 18:48:07 -05:00
|
|
|
_, catExists := categories[href]
|
|
|
|
if !catExists {
|
2023-02-14 18:21:17 -05:00
|
|
|
return
|
|
|
|
}
|
2022-08-30 09:20:58 -04:00
|
|
|
|
2023-02-14 18:21:17 -05:00
|
|
|
// FIXME: parse url
|
|
|
|
uri := strings.SplitAfter(href, "#")
|
|
|
|
if len(uri) >= 2 && uri[1] != "contents" {
|
|
|
|
s.SetAttr("href", uri[1])
|
|
|
|
}
|
|
|
|
})
|
2021-12-25 09:32:20 -05:00
|
|
|
|
2023-02-14 12:50:14 -05:00
|
|
|
fmt.Printf("Rewrite links in Index file: %s\n", outIndexFile)
|
2023-02-14 18:21:17 -05:00
|
|
|
resultHtml, err := doc.Html()
|
2023-02-14 17:26:55 -05:00
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to render html: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := os.WriteFile(outIndexFile, []byte(resultHtml), 0644); err != nil {
|
|
|
|
return fmt.Errorf("unable to rewrite index file: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
2021-12-25 09:32:20 -05:00
|
|
|
}
|