1/**
  2  Generate web pages using a common template plus individual
  3  directories containing files for Title, Banner text, and Content.
  4  The `public` directory has a file named `template.html` that
  5  contains format verbs "%s" to insert text. This program walks
  6  through each directory in `public` and adds content to the template,
  7  generating the directory's `index.html` file. File structures look
  8  like this:
  9
 10  public
 11  ├── about
 12  │   ├── banner.txt
 13  │   ├── content.html
 14  │   ├── index.html
 15  │   └── title.txt
 16  ├── contact
 17  │   ├── banner.txt
 18  │   ├── content.html
 19  │   ├── index.html
 20  │   └── title.txt
 21  └── template.html
 22
 23  The directory name becomes an HTTP path, and the web server
 24  recognises the `index.html` to serve content. Each directory's
 25  `index.html` is generated when `content.html` is newer.
 26  Each page is generated using a separate go routine as needed.
 27*/
 28package main
 29
 30import (
 31	"fmt"
 32	"io/fs"
 33	"io/ioutil"
 34	"log"
 35	"os"
 36	"strings"
 37	"sync"
 38	"time"
 39)
 40
 41var wg = &sync.WaitGroup{} // Global wait group to allow access from subroutines.
 42
 43// Helper to make error checking easier.
 44func check(e error) {
 45	if e != nil {
 46		log.Fatal(e)
 47	}
 48}
 49
 50/**
 51  Look at each file in the 'public' directory, and pass it to the go routine
 52  that generates an 'index.html' for each directory.
 53*/
 54func main() {
 55	err := os.Chdir("public") // This program runs from the project home directory.
 56	check(err)
 57	thisDir, err := os.Getwd()
 58	check(err)
 59	files, _ := ioutil.ReadDir(thisDir)
 60	/**
 61	  We spawn a go routine for each file, and depend on the routine to
 62	  release the lock when finished. Otherwise, the main routine will
 63	  terminate before the spawned go routines are finished and some
 64	  files will not be processed.
 65	*/
 66	for _, file := range files {
 67		wg.Add(1)
 68		fileInfo := os.FileInfo(file)
 69		go applyTemplate(fileInfo)
 70		wg.Wait() // The child routine releases the resource when finished.
 71	}
 72}
 73
 74/**
 75  Each directory has three files: title.txt, banner.txt, and
 76  content.html.  The template file is an html template with three "%s"
 77  verbs embedded in the text literals, which will be included with the
 78  file contents by fmt.Sprintf(). The resulting string is written out
 79  to index.html.  After the index.html is written, the other files are
 80  no longer needed, but kept for regeneration of index.html when
 81  content.html is changed.
 82*/
 83func applyTemplate(stat fs.FileInfo) {
 84	defer wg.Done()
 85	if !stat.IsDir() { // Skip ordinary files.
 86		return
 87	}
 88	// Some directories have non-generating content, so skip.
 89	if stat.Name() == "resume" || stat.Name() == "fonts" || stat.Name() == "docs" {
 90		return
 91	}
 92	err := os.Chdir(stat.Name()) // Enter the directory for this web page.
 93	check(err)
 94	// Get file stats for timestamp checking to see if an update is needed.
 95	contentStat, err := os.Lstat("content.html")
 96	check(err)
 97	indexStat, err := os.Lstat("index.html")
 98	check(err)
 99	/**
100	  Check timestamp of input "content.html" to see if it's newer than
101	  generated "index.html". Return if update isn't needed.
102	  In the unlikely event that "title.txt" or "banner.txt" are updated, run
103	  `touch content.html` first to force an update.
104	*/
105	diff := contentStat.ModTime().Sub(indexStat.ModTime())
106	if diff < (time.Duration(0) * time.Second) { // content.html not updated, so bail.
107		err = os.Chdir("..") // Return for next run.
108		check(err)
109		return
110	}
111	/**
112	  Generate new index.html file.  Read input files and generate a new
113	  page. Note that 'template.html' is shared by all pages, but the
114	  others are unique. It's assumed that 'title.txt' and 'banner.txt'
115	  are stable and rarely need updating.
116	*/
117	fmt.Printf("%v content is newer, so updating index.html.\n", stat.Name())
118	template := getFileContents("../template.html") // In shared public directory.
119	title := getFileContents("title.txt")
120	banner := getFileContents("banner.txt")
121	content := getFileContents("content.html")
122	t := strings.TrimSuffix(title, "\n")
123	b := strings.TrimSuffix(banner, "\n")
124	out := fmt.Sprintf(template, t, b, content) // template's "%s" verbs filled in.
125	err = os.WriteFile("index.html", []byte(out), 0644)
126	check(err)
127	err = os.Chdir("..") // Return to 'public' directory for the next run.
128	check(err)
129}
130
131func getFileContents(fileName string) string {
132	data, err := os.ReadFile(fileName)
133	check(err)
134	return string(data)
135}