commit cfcd2ed6eb61656a8fcf75e417336624c7f26a8e
Author: m15o
Date: Tue Nov 2 06:28:59 2021 +0100
first commit
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..dd05eb9
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+.idea
+bin
+*~
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..867476b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,2 @@
+build:
+ CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o bin/ni main.go
diff --git a/gmi2html/convert.go b/gmi2html/convert.go
new file mode 100644
index 0000000..59fd38f
--- /dev/null
+++ b/gmi2html/convert.go
@@ -0,0 +1,114 @@
+package gmi2html
+
+import (
+ "html"
+ "regexp"
+ "strings"
+)
+
+var heading1Regexp = regexp.MustCompile("^# (.*)$")
+var heading2Regexp = regexp.MustCompile("^## (.*)$")
+var heading3Regexp = regexp.MustCompile("^### (.*)$")
+var linkRegexp = regexp.MustCompile("^=> ([^\\s]+) ?(.+)?$")
+var blockquoteRegexp = regexp.MustCompile("^> (.*)$")
+var preRegexp = regexp.MustCompile("^```.*$")
+var bulletRegexp = regexp.MustCompile(`^\* ?(.*)$`)
+
+func clearLinkMode(linkMode *bool, rv *[]string) {
+ if *linkMode {
+ *rv = append(*rv, "
")
+ *linkMode = false
+ }
+}
+
+func clearUlMode(ulMode *bool, rv *[]string) {
+ if *ulMode {
+ *rv = append(*rv, "")
+ *ulMode = false
+ }
+}
+
+func sanitize(input string) string {
+ return html.EscapeString(input)
+}
+
+func Convert(gmi string) string {
+ var rv []string
+ preMode := false
+ ulMode := false
+ linkMode := false
+ for _, l := range strings.Split(gmi, "\n") {
+ l = strings.TrimRight(l, "\r")
+ if preMode {
+ switch {
+ case preRegexp.MatchString(l):
+ rv = append(rv, "")
+ preMode = false
+ default:
+ rv = append(rv, sanitize(l))
+ }
+ } else {
+ switch {
+ case heading1Regexp.MatchString(l):
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ matches := heading1Regexp.FindStringSubmatch(l)
+ rv = append(rv, ""+sanitize(matches[1])+"
")
+ case heading2Regexp.MatchString(l):
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ matches := heading2Regexp.FindStringSubmatch(l)
+ rv = append(rv, ""+sanitize(matches[1])+"
")
+ case heading3Regexp.MatchString(l):
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ matches := heading3Regexp.FindStringSubmatch(l)
+ rv = append(rv, ""+sanitize(matches[1])+"
")
+ case blockquoteRegexp.MatchString(l):
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ matches := blockquoteRegexp.FindStringSubmatch(l)
+ rv = append(rv, ""+sanitize(matches[1])+"
")
+ case linkRegexp.MatchString(l):
+ clearUlMode(&ulMode, &rv)
+ matches := linkRegexp.FindStringSubmatch(l)
+ if len(matches[2]) == 0 {
+ matches[2] = matches[1]
+ }
+ if strings.HasSuffix(matches[1], ".png") || strings.HasSuffix(matches[1], ".PNG") || strings.HasSuffix(matches[1], ".jpg") || strings.HasSuffix(matches[1], ".JPG") || strings.HasSuffix(matches[1], ".jpeg") || strings.HasSuffix(matches[1], ".gif") || strings.HasSuffix(matches[1], ".GIF") {
+ rv = append(rv, "")
+ continue
+ }
+ if linkMode {
+ rv = append(rv, ""+sanitize(matches[2])+"
")
+ continue
+ }
+ rv = append(rv, ""+sanitize(matches[2])+"
")
+ linkMode = true
+ case preRegexp.MatchString(l):
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ rv = append(rv, "
")
+ preMode = true
+ case bulletRegexp.MatchString(l):
+ clearLinkMode(&linkMode, &rv)
+ matches := bulletRegexp.FindStringSubmatch(l)
+ if ulMode {
+ rv = append(rv, "
"+sanitize(matches[1])+"")
+ continue
+ }
+ rv = append(rv, "\n- "+sanitize(matches[1])+"
")
+ ulMode = true
+ default:
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ if len(l) != 0 {
+ rv = append(rv, ""+sanitize(l)+"
")
+ }
+ }
+ }
+ }
+ clearUlMode(&ulMode, &rv)
+ clearLinkMode(&linkMode, &rv)
+ return strings.Join(rv, "\n")
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..a5fa878
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,3 @@
+module ni
+
+go 1.16
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..8b561c9
--- /dev/null
+++ b/main.go
@@ -0,0 +1,156 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "html/template"
+ "io/ioutil"
+ "log"
+ "ni/gmi2html"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "time"
+)
+
+var tplPage = `
+{{ define "content" }}
+{{ .Content }}
+{{ if .Backlinks }}
+Backlinks
+
+{{ range .Backlinks }}
+- {{ . }}
+{{ end }}
+
+{{ end }}
+{{ end }}
+`
+
+var tplChangelog = `
+{{ define "content" }}
+
+{{ range . }}
+- {{ .Name }} {{ .TimeFormatted }}
+{{ end }}
+
+{{ end }}
+`
+
+var filename = regexp.MustCompile(`^[a-z0-9\-]+\.gmi$`)
+var re = regexp.MustCompile(`\[\[([a-z0-9\-]+)\]\]`)
+var PageContent *template.Template
+var PageChangelog *template.Template
+
+func backlinks(in, name string) []string {
+ var rv []string
+ bl, err := exec.Command("/bin/sh", "-c", "grep -l '\\[\\["+name[:len(name)-4]+"\\]\\]' "+in+"/*.gmi").Output()
+ if err != nil {
+ return rv
+ }
+ for _, link := range strings.Fields(string(bl)) {
+ oname := outputName(link[len(in+"/"):])
+ if oname != outputName(name) {
+ rv = append(rv, oname)
+ }
+ }
+ return rv
+}
+
+func process(in, name string, d []byte) []byte {
+ html := bytes.NewBufferString("")
+
+ err := PageContent.Execute(html, map[string]interface{}{
+ "Title": name,
+ "Content": template.HTML(re.ReplaceAllString(gmi2html.Convert(string(d)), `$1`)),
+ "Backlinks": backlinks(in, name),
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ return html.Bytes()
+}
+
+func buildChangelog(f []File) []byte {
+ html := bytes.NewBufferString("")
+ err := PageChangelog.Execute(html, f)
+ if err != nil {
+ log.Fatal(err)
+ }
+ return html.Bytes()
+}
+
+type File struct {
+ Name string
+ UpdatedAt time.Time
+ TimeFormatted string
+}
+
+func outputName(inputName string) string {
+ return inputName[:len(inputName)-3] + "html"
+}
+
+func main() {
+ if len(os.Args) != 4 {
+ log.Fatal("Usage: ni input output template.html")
+ }
+
+ in := os.Args[1]
+ out := os.Args[2]
+ tpl := os.Args[3]
+
+ b, err := os.ReadFile(tpl)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ PageContent = template.Must(template.New("").Parse(string(b) + tplPage))
+ PageChangelog = template.Must(template.New("").Parse(string(b) + tplChangelog))
+
+ files, err := ioutil.ReadDir(in)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ var changelog []File
+
+ for _, file := range files {
+ if !filename.MatchString(file.Name()) {
+ continue
+ }
+ file, err := os.Stat(filepath.Join(in, file.Name()))
+ if err != nil {
+ log.Fatal(err)
+ }
+ updatedAt := file.ModTime()
+ changelog = append(changelog, File{
+ UpdatedAt: updatedAt,
+ TimeFormatted: updatedAt.Format("2006-01-02"),
+ Name: outputName(file.Name()),
+ })
+ data, err := os.ReadFile(filepath.Join(in, file.Name()))
+ if err != nil {
+ log.Fatal(err)
+ }
+ output := process(in, file.Name(), data)
+ err = os.WriteFile(filepath.Join(out, outputName(file.Name())), output, 0644)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Print(".")
+ }
+ sort.Slice(changelog, func(i, j int) bool {
+ return changelog[i].UpdatedAt.After(changelog[j].UpdatedAt)
+ })
+ if err != nil {
+ log.Fatal(err)
+ }
+ err = os.WriteFile(filepath.Join(out, "changelog.html"), buildChangelog(changelog), 0644)
+ if err != nil {
+ log.Fatal(err)
+ }
+ fmt.Println("Done")
+}