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
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.0-20200928222852-855eff6d8802/go.mod h1:4MWQDsleal4HRi/LuxxM6ymWJQikP3Gh7xZindVCHzg=
git.sr.ht/~adnano/gmi v0.0.1-alpha h1:6SRpwsKRowrKfExhyYQaQ0SBYcMIhHG4hUaWF67iP4k=
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-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

72
kiln.go
View file

@ -1,6 +1,7 @@
package main
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
@ -72,7 +73,7 @@ func (s *Site) Write(dstDir string) error {
}
// Write the directory
return s.Directory.Write(dstDir)
return s.Directory.Write(dstDir, OutputGemini)
}
// 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
if dir.Index == nil {
path := filepath.Join(dir.Path, "index.gmi")
builder := &strings.Builder{}
var b bytes.Buffer
tmpl := s.Templates.Lookup("index.gmi")
if tmpl != nil {
if err := tmpl.Execute(builder, dir); err != nil {
if err := tmpl.Execute(&b, dir); err != nil {
return err
}
content := builder.String()
content := b.Bytes()
permalink := filepath.Dir(path)
if permalink == "." {
permalink = ""
@ -94,7 +95,7 @@ func (s *Site) Manipulate(dir *Directory) error {
page := &Page{
Path: path,
Permalink: "/" + permalink,
Content: content,
content: content,
}
dir.Index = page
}
@ -102,12 +103,12 @@ func (s *Site) Manipulate(dir *Directory) error {
// Manipulate pages
for i := range dir.Pages {
builder := &strings.Builder{}
var b bytes.Buffer
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
}
dir.Pages[i].Content = builder.String()
dir.Pages[i].content = b.Bytes()
}
return nil
@ -125,13 +126,17 @@ type Page struct {
// Ex: 2020-09-22-hello-world.gmi
Date time.Time
// 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?")
// 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
var date time.Time
const layout = "2006-01-02"
@ -160,18 +165,20 @@ func NewPage(path string, content string) *Page {
// Try to parse the title from the contents
var title string
if submatches := titleRE.FindStringSubmatch(content); submatches != nil {
title = submatches[1]
if submatches := titleRE.FindSubmatch(content); submatches != nil {
title = string(submatches[1])
// Remove the title from the contents
content = content[len(submatches[0]):]
}
permalink := strings.TrimSuffix(path, ".gmi")
return &Page{
Path: path,
Permalink: "/" + path,
Permalink: "/" + permalink + "/",
Title: title,
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) {
case ".gmi", ".gemini":
// Gather page data
content := string(content)
page := NewPage(path, content)
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.
func (d *Directory) Write(dstDir string) error {
func (d *Directory) Write(dstDir string, format OutputFormat) error {
// Create the directory
dirPath := filepath.Join(dstDir, d.Path)
if err := os.MkdirAll(dirPath, 0755); err != nil {
@ -257,33 +263,53 @@ func (d *Directory) Write(dstDir string) error {
// Write the files
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)
if err != nil {
return err
}
data := []byte(page.Content)
if _, err := f.Write(data); err != nil {
if _, err := f.Write(content); err != nil {
return err
}
}
// Write the index file
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)
if err != nil {
return err
}
data := []byte(d.Index.Content)
if _, err := f.Write(data); err != nil {
if _, err := f.Write(content); err != nil {
return err
}
}
// Write subdirectories
for _, dir := range d.Directories {
dir.Write(dstDir)
dir.Write(dstDir, format)
}
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 (
serveSite bool
toHtml bool
)
func init() {
flag.BoolVar(&serveSite, "serve", false, "serve the site")
flag.BoolVar(&toHtml, "html", false, "output HTML")
}
func main() {
@ -39,6 +41,11 @@ func build() error {
if err := site.Write("dst"); err != nil {
return err
}
if toHtml {
if err := site.Directory.Write("dst.html", OutputHTML); err != nil {
return err
}
}
return nil
}

129
render.go
View file

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