Improve HTML renderer

This commit is contained in:
adnano 2020-09-29 10:57:15 -04:00
parent 8b811c4e23
commit fb1b053da4
5 changed files with 115 additions and 99 deletions

2
go.mod
View file

@ -2,4 +2,4 @@ module kiln
go 1.15 go 1.15
require git.sr.ht/~adnano/gmi v0.0.0-20200928222852-855eff6d8802 require git.sr.ht/~adnano/gmi v0.0.1-alpha

4
go.sum
View file

@ -1,5 +1,5 @@
git.sr.ht/~adnano/gmi v0.0.0-20200928222852-855eff6d8802 h1:VAuDKKZWyq3/UkBuIeLEQ1qjRqiRfACfErdvBNdYz9A= git.sr.ht/~adnano/gmi v0.0.1-alpha h1:6SRpwsKRowrKfExhyYQaQ0SBYcMIhHG4hUaWF67iP4k=
git.sr.ht/~adnano/gmi v0.0.0-20200928222852-855eff6d8802/go.mod h1:4MWQDsleal4HRi/LuxxM6ymWJQikP3Gh7xZindVCHzg= git.sr.ht/~adnano/gmi v0.0.1-alpha/go.mod h1:4MWQDsleal4HRi/LuxxM6ymWJQikP3Gh7xZindVCHzg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

72
kiln.go
View file

@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -72,7 +73,7 @@ func (s *Site) Write(dstDir string) error {
} }
// Write the directory // Write the directory
return s.Directory.Write(dstDir) return s.Directory.Write(dstDir, OutputGemini)
} }
// Manipulate processes and manipulates the site's content. // Manipulate processes and manipulates the site's content.
@ -80,13 +81,13 @@ func (s *Site) Manipulate(dir *Directory) error {
// Write the directory index file, if it doesn't exist // Write the directory index file, if it doesn't exist
if dir.Index == nil { if dir.Index == nil {
path := filepath.Join(dir.Path, "index.gmi") path := filepath.Join(dir.Path, "index.gmi")
builder := &strings.Builder{} var b bytes.Buffer
tmpl := s.Templates.Lookup("index.gmi") tmpl := s.Templates.Lookup("index.gmi")
if tmpl != nil { if tmpl != nil {
if err := tmpl.Execute(builder, dir); err != nil { if err := tmpl.Execute(&b, dir); err != nil {
return err return err
} }
content := builder.String() content := b.Bytes()
permalink := filepath.Dir(path) permalink := filepath.Dir(path)
if permalink == "." { if permalink == "." {
permalink = "" permalink = ""
@ -94,7 +95,7 @@ func (s *Site) Manipulate(dir *Directory) error {
page := &Page{ page := &Page{
Path: path, Path: path,
Permalink: "/" + permalink, Permalink: "/" + permalink,
Content: content, content: content,
} }
dir.Index = page dir.Index = page
} }
@ -102,12 +103,12 @@ func (s *Site) Manipulate(dir *Directory) error {
// Manipulate pages // Manipulate pages
for i := range dir.Pages { for i := range dir.Pages {
builder := &strings.Builder{} var b bytes.Buffer
tmpl := s.Templates.Lookup("page.gmi") tmpl := s.Templates.Lookup("page.gmi")
if err := tmpl.Execute(builder, dir.Pages[i]); err != nil { if err := tmpl.Execute(&b, dir.Pages[i]); err != nil {
return err return err
} }
dir.Pages[i].Content = builder.String() dir.Pages[i].content = b.Bytes()
} }
return nil return nil
@ -125,13 +126,17 @@ type Page struct {
// Ex: 2020-09-22-hello-world.gmi // Ex: 2020-09-22-hello-world.gmi
Date time.Time Date time.Time
// The content of this page. // The content of this page.
Content string content []byte
}
func (p *Page) Content() string {
return string(p.content)
} }
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?") var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?")
// NewPage returns a new Page with the given path and content. // NewPage returns a new Page with the given path and content.
func NewPage(path string, content string) *Page { func NewPage(path string, content []byte) *Page {
// Try to parse the date from the page filename // Try to parse the date from the page filename
var date time.Time var date time.Time
const layout = "2006-01-02" const layout = "2006-01-02"
@ -160,18 +165,20 @@ func NewPage(path string, content string) *Page {
// Try to parse the title from the contents // Try to parse the title from the contents
var title string var title string
if submatches := titleRE.FindStringSubmatch(content); submatches != nil { if submatches := titleRE.FindSubmatch(content); submatches != nil {
title = submatches[1] title = string(submatches[1])
// Remove the title from the contents // Remove the title from the contents
content = content[len(submatches[0]):] content = content[len(submatches[0]):]
} }
permalink := strings.TrimSuffix(path, ".gmi")
return &Page{ return &Page{
Path: path, Path: path,
Permalink: "/" + path, Permalink: "/" + permalink + "/",
Title: title, Title: title,
Date: date, Date: date,
Content: content, content: content,
} }
} }
@ -229,7 +236,6 @@ func (d *Directory) Read(site *Site, srcDir string, path string) error {
switch filepath.Ext(name) { switch filepath.Ext(name) {
case ".gmi", ".gemini": case ".gmi", ".gemini":
// Gather page data // Gather page data
content := string(content)
page := NewPage(path, content) page := NewPage(path, content)
if name == "index.gmi" { if name == "index.gmi" {
@ -248,7 +254,7 @@ func (d *Directory) Read(site *Site, srcDir string, path string) error {
} }
// Write writes the Directory to the provided destination path. // Write writes the Directory to the provided destination path.
func (d *Directory) Write(dstDir string) error { func (d *Directory) Write(dstDir string, format OutputFormat) error {
// Create the directory // Create the directory
dirPath := filepath.Join(dstDir, d.Path) dirPath := filepath.Join(dstDir, d.Path)
if err := os.MkdirAll(dirPath, 0755); err != nil { if err := os.MkdirAll(dirPath, 0755); err != nil {
@ -257,33 +263,53 @@ func (d *Directory) Write(dstDir string) error {
// Write the files // Write the files
for _, page := range d.Pages { for _, page := range d.Pages {
dstPath := filepath.Join(dstDir, page.Path) path, content := format(page)
dstPath := filepath.Join(dstDir, path)
dir := filepath.Dir(dstPath)
os.MkdirAll(dir, 0755)
f, err := os.Create(dstPath) f, err := os.Create(dstPath)
if err != nil { if err != nil {
return err return err
} }
data := []byte(page.Content) if _, err := f.Write(content); err != nil {
if _, err := f.Write(data); err != nil {
return err return err
} }
} }
// Write the index file // Write the index file
if d.Index != nil { if d.Index != nil {
dstPath := filepath.Join(dstDir, d.Index.Path) path, content := format(d.Index)
dstPath := filepath.Join(dstDir, path)
f, err := os.Create(dstPath) f, err := os.Create(dstPath)
if err != nil { if err != nil {
return err return err
} }
data := []byte(d.Index.Content) if _, err := f.Write(content); err != nil {
if _, err := f.Write(data); err != nil {
return err return err
} }
} }
// Write subdirectories // Write subdirectories
for _, dir := range d.Directories { for _, dir := range d.Directories {
dir.Write(dstDir) dir.Write(dstDir, format)
} }
return nil return nil
} }
// OutputFormat represents an output format.
type OutputFormat func(*Page) (path string, content []byte)
func OutputGemini(p *Page) (path string, content []byte) {
const indexPath = "index.gmi"
path = filepath.Join(p.Permalink, indexPath)
content = p.content
return
}
func OutputHTML(p *Page) (path string, content []byte) {
const indexPath = "index.html"
path = filepath.Join(p.Permalink, indexPath)
r := bytes.NewReader(p.content)
content = GeminiToHTML(r)
return
}

View file

@ -10,10 +10,12 @@ import (
var ( var (
serveSite bool serveSite bool
toHtml bool
) )
func init() { func init() {
flag.BoolVar(&serveSite, "serve", false, "serve the site") flag.BoolVar(&serveSite, "serve", false, "serve the site")
flag.BoolVar(&toHtml, "html", false, "output HTML")
} }
func main() { func main() {
@ -39,6 +41,11 @@ func build() error {
if err := site.Write("dst"); err != nil { if err := site.Write("dst"); err != nil {
return err return err
} }
if toHtml {
if err := site.Directory.Write("dst.html", OutputHTML); err != nil {
return err
}
}
return nil return nil
} }

129
render.go
View file

@ -1,89 +1,72 @@
package main package main
import ( import (
"bufio" "bytes"
"fmt"
"html"
"io" "io"
"strings"
"git.sr.ht/~adnano/gmi"
) )
// GeminiToHTML reads Gemini from the provided reader and returns an HTML string. // GeminiToHTML reads Gemini from the provided reader and returns an HTML string.
func GeminiToHTML(r io.Reader) string { func GeminiToHTML(r io.Reader) []byte {
const spacetab = " \t" var b bytes.Buffer
var pre bool
var list bool
type parseCtx int b.WriteString("<link rel='stylesheet' href='/style.css'>\n")
const ( text := gmi.Parse(r)
parseCtxNone = iota for _, l := range text {
parseCtxPre if _, ok := l.(gmi.LineListItem); ok {
parseCtxList if !list {
) list = true
b.WriteString("<ul>\n")
var builder strings.Builder }
var ctx parseCtx } else if list {
list = false
scanner := bufio.NewScanner(r) b.WriteString("</ul>\n")
for scanner.Scan() {
line := scanner.Text()
if len(line) == 0 {
continue
} }
if strings.HasPrefix(line, "```") { switch l.(type) {
if ctx != parseCtxPre { case gmi.LineLink:
builder.WriteString("<pre>\n") link := l.(gmi.LineLink)
ctx = parseCtxPre url := html.EscapeString(link.URL)
name := html.EscapeString(link.Name)
if name == "" {
name = url
}
fmt.Fprintf(&b, "<p><a href='%s'>%s</a></p>", url, name)
case gmi.LinePreformattingToggle:
pre = !pre
if pre {
b.WriteString("<pre>\n")
} else { } else {
builder.WriteString("</pre>\n") b.WriteString("</pre>\n")
ctx = parseCtxNone
} }
} else if ctx == parseCtxPre { case gmi.LinePreformattedText:
builder.WriteString(line) text := string(l.(gmi.LinePreformattedText))
builder.WriteString("\n") fmt.Fprintf(&b, "%s\n", html.EscapeString(text))
} else if strings.HasPrefix(line, "*") { case gmi.LineHeading1:
if ctx != parseCtxList { text := string(l.(gmi.LineHeading1))
builder.WriteString("<ul>\n") fmt.Fprintf(&b, "<h1>%s</h1>\n", html.EscapeString(text))
ctx = parseCtxList case gmi.LineHeading2:
} text := string(l.(gmi.LineHeading2))
builder.WriteString("<li>") fmt.Fprintf(&b, "<h2>%s</h2>\n", html.EscapeString(text))
line = line[1:] case gmi.LineHeading3:
builder.WriteString(strings.TrimLeft(line, spacetab)) text := string(l.(gmi.LineHeading3))
builder.WriteString("</li>\n") fmt.Fprintf(&b, "<h3>%s</h3>\n", html.EscapeString(text))
} else if ctx == parseCtxList { case gmi.LineListItem:
builder.WriteString("</ul>\n") text := string(l.(gmi.LineListItem))
ctx = parseCtxNone fmt.Fprintf(&b, "<li>%s</li>\n", html.EscapeString(text))
} else if strings.HasPrefix(line, "###") { case gmi.LineQuote:
line = line[3:] text := string(l.(gmi.LineQuote))
line = strings.TrimLeft(line, spacetab) fmt.Fprintf(&b, "<blockquote>%s</blockquote>\n", html.EscapeString(text))
builder.WriteString("<h3>" + line + "</h3>\n") case gmi.LineText:
} else if strings.HasPrefix(line, "##") { text := string(l.(gmi.LineText))
line = line[2:] fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
line = strings.TrimLeft(line, spacetab)
builder.WriteString("<h2>" + line + "</h2>\n")
} else if strings.HasPrefix(line, "#") {
line = line[1:]
line = strings.TrimLeft(line, spacetab)
builder.WriteString("<h1>" + line + "</h1>\n")
} else if strings.HasPrefix(line, "=>") {
line = line[2:]
line = strings.TrimLeft(line, spacetab)
split := strings.IndexAny(line, spacetab)
url := line[:split]
name := line[split:]
name = strings.TrimLeft(name, spacetab)
builder.WriteString("<p><a href='" + url + "'>" + name + "</a></p>\n")
} else if strings.HasPrefix(line, ">") {
builder.WriteString("<blockquote>" + line + "</blockquote>")
} else {
builder.WriteString("<p>" + line + "</p>\n")
} }
} }
return b.Bytes()
// Close any opened tags
switch ctx {
case parseCtxPre:
builder.WriteString("</pre>\n")
case parseCtxList:
builder.WriteString("</ul>\n")
}
return builder.String()
} }