mirror of
https://git.sr.ht/~adnano/kiln
synced 2024-10-30 01:13:08 +00:00
Add support for Gemini feeds
This commit is contained in:
parent
3c4d934f55
commit
f14999d88a
7
Makefile
7
Makefile
|
@ -5,6 +5,7 @@
|
||||||
VERSION=0.0.0
|
VERSION=0.0.0
|
||||||
|
|
||||||
PREFIX?=/usr/local
|
PREFIX?=/usr/local
|
||||||
|
BINDIR?=$(PREFIX)/bin
|
||||||
MANDIR?=$(PREFIX)/share/man
|
MANDIR?=$(PREFIX)/share/man
|
||||||
|
|
||||||
VPATH=doc
|
VPATH=doc
|
||||||
|
@ -35,12 +36,12 @@ clean:
|
||||||
$(RM) $(DOCS)
|
$(RM) $(DOCS)
|
||||||
|
|
||||||
install: all
|
install: all
|
||||||
mkdir -p $(DESTDIR)$(PREFIX)/bin $(DESTDIR)$(MANDIR)/man1
|
mkdir -p $(DESTDIR)$(BINDIR) $(DESTDIR)$(MANDIR)/man1
|
||||||
install -m755 kiln $(DESTDIR)$(PREFIX)/bin
|
install -m755 kiln $(DESTDIR)$(BINDIR)/kiln
|
||||||
install -m755 kiln.1 $(MANDIR)/man1/kiln.1
|
install -m755 kiln.1 $(MANDIR)/man1/kiln.1
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
$(RM) $(DESTDIR)$(PREFIX)/bin/kiln
|
$(RM) $(DESTDIR)$(BINDIR)/kiln
|
||||||
$(RM) $(DESTDIR)$(MANDIR)/man1/kiln.1
|
$(RM) $(DESTDIR)$(MANDIR)/man1/kiln.1
|
||||||
|
|
||||||
.PHONY: all doc clean install uninstall
|
.PHONY: all doc clean install uninstall
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# kiln
|
# kiln
|
||||||
|
|
||||||
A simple static site generator for Gemini.
|
A simple static site generator for Gemini sites.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Simple and fast
|
- Simple and fast
|
||||||
- Gemini support
|
- Gemini support
|
||||||
|
- Generate Gemini feeds
|
||||||
- Go templates
|
- Go templates
|
||||||
- Export to HTML
|
- Output HTML
|
||||||
- Generate Atom feeds
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
62
config.go
Normal file
62
config.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"git.sr.ht/~adnano/go-ini"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config contains site configuration.
|
||||||
|
type Config struct {
|
||||||
|
Title string
|
||||||
|
Feeds map[string]string
|
||||||
|
Templates *Templates
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConfig returns a new configuration.
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{
|
||||||
|
Feeds: map[string]string{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads the configuration from the provided path.
|
||||||
|
func (c *Config) Load(path string) error {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load config
|
||||||
|
return ini.Parse(f, func(section, key, value string) {
|
||||||
|
switch section {
|
||||||
|
case "":
|
||||||
|
switch key {
|
||||||
|
case "title":
|
||||||
|
c.Title = value
|
||||||
|
}
|
||||||
|
case "feeds":
|
||||||
|
c.Feeds[key] = value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTemplates loads templates from the provided path.
|
||||||
|
func (c *Config) LoadTemplates(path string) error {
|
||||||
|
// Site contains site metadata passed to templates
|
||||||
|
type Site struct {
|
||||||
|
Title string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load templates
|
||||||
|
c.Templates = NewTemplates()
|
||||||
|
c.Templates.Funcs(template.FuncMap{
|
||||||
|
"site": func() Site {
|
||||||
|
return Site{
|
||||||
|
Title: c.Title,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return c.Templates.Load(path)
|
||||||
|
}
|
201
dir.go
Normal file
201
dir.go
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
pathpkg "path"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dir represents a directory.
|
||||||
|
type Dir struct {
|
||||||
|
Path string // Directory path.
|
||||||
|
Pages []*Page // Pages in this directory.
|
||||||
|
Dirs []*Dir // Subdirectories.
|
||||||
|
files map[string][]byte // Static files.
|
||||||
|
index *Page // The index page.
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDir returns a new Dir with the given path.
|
||||||
|
func NewDir(path string) *Dir {
|
||||||
|
if path == "" {
|
||||||
|
path = "/"
|
||||||
|
} else {
|
||||||
|
path = "/" + path + "/"
|
||||||
|
}
|
||||||
|
return &Dir{
|
||||||
|
Path: path,
|
||||||
|
files: map[string][]byte{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// read reads from a directory and indexes the files and directories within it.
|
||||||
|
func (d *Dir) read(srcDir string, path string) error {
|
||||||
|
entries, err := ioutil.ReadDir(pathpkg.Join(srcDir, path))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
name := entry.Name()
|
||||||
|
// Ignore names that start with "_"
|
||||||
|
if strings.HasPrefix(name, "_") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
path := pathpkg.Join(path, name)
|
||||||
|
if entry.IsDir() {
|
||||||
|
// Gather directory data
|
||||||
|
dir := NewDir(path)
|
||||||
|
if err := dir.read(srcDir, path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Dirs = append(d.Dirs, dir)
|
||||||
|
} else {
|
||||||
|
srcPath := pathpkg.Join(srcDir, path)
|
||||||
|
content, err := ioutil.ReadFile(srcPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if pathpkg.Ext(name) == ".gmi" {
|
||||||
|
// Gather page data
|
||||||
|
page := NewPage(path, content)
|
||||||
|
if name == "index.gmi" {
|
||||||
|
d.index = page
|
||||||
|
} else {
|
||||||
|
d.Pages = append(d.Pages, page)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Static file
|
||||||
|
d.files[path] = content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// manipulate processes and manipulates the directory's contents.
|
||||||
|
func (d *Dir) manipulate(cfg *Config) error {
|
||||||
|
// Create the directory index file, if it doesn't exist
|
||||||
|
if d.index == nil {
|
||||||
|
var b strings.Builder
|
||||||
|
tmpl := cfg.Templates.FindTemplate(d.Path, "index.gmi")
|
||||||
|
if err := tmpl.Execute(&b, d); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.index = &Page{
|
||||||
|
Path: d.Path,
|
||||||
|
Content: b.String(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manipulate pages
|
||||||
|
for i := range d.Pages {
|
||||||
|
var b strings.Builder
|
||||||
|
tmpl := cfg.Templates.FindTemplate(d.Pages[i].Path, "page.gmi")
|
||||||
|
if err := tmpl.Execute(&b, d.Pages[i]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Pages[i].Content = b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feed represents a feed.
|
||||||
|
type Feed struct {
|
||||||
|
Title string // Feed title.
|
||||||
|
Updated time.Time // Last updated time.
|
||||||
|
Entries []*Page // Feed entries.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create feeds
|
||||||
|
if title, ok := cfg.Feeds[d.Path]; ok {
|
||||||
|
var b strings.Builder
|
||||||
|
feed := &Feed{
|
||||||
|
Title: title,
|
||||||
|
Updated: time.Now(),
|
||||||
|
Entries: d.Pages,
|
||||||
|
}
|
||||||
|
tmpl := cfg.Templates.FindTemplate(d.Path, "feed.gmi")
|
||||||
|
if err := tmpl.Execute(&b, feed); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
d.Pages = append(d.Pages, &Page{
|
||||||
|
Path: pathpkg.Join(d.Path, "feed"),
|
||||||
|
Content: b.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manipulate subdirectories
|
||||||
|
for _, d := range d.Dirs {
|
||||||
|
if err := d.manipulate(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// write writes the Dir to the provided destination path.
|
||||||
|
func (d *Dir) write(dstDir string, format outputFormat, cfg *Config) error {
|
||||||
|
// Create the directory
|
||||||
|
dirPath := pathpkg.Join(dstDir, d.Path)
|
||||||
|
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write static files
|
||||||
|
for path := range d.files {
|
||||||
|
dstPath := pathpkg.Join(dstDir, path)
|
||||||
|
f, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data := d.files[path]
|
||||||
|
if _, err := f.Write(data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the pages
|
||||||
|
for _, page := range d.Pages {
|
||||||
|
path, content := format(page, cfg)
|
||||||
|
dstPath := pathpkg.Join(dstDir, path)
|
||||||
|
dir := pathpkg.Dir(dstPath)
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
f, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the index file
|
||||||
|
if d.index != nil {
|
||||||
|
path, content := format(d.index, cfg)
|
||||||
|
dstPath := pathpkg.Join(dstDir, path)
|
||||||
|
f, err := os.Create(dstPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := f.Write(content); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write subdirectories
|
||||||
|
for _, dir := range d.Dirs {
|
||||||
|
dir.write(dstDir, format, cfg)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sort sorts the directory's pages by date.
|
||||||
|
func (d *Dir) sort() {
|
||||||
|
sort.Slice(d.Pages, func(i, j int) bool {
|
||||||
|
return d.Pages[i].Date.After(d.Pages[j].Date)
|
||||||
|
})
|
||||||
|
// Sort subdirectories
|
||||||
|
for _, d := range d.Dirs {
|
||||||
|
d.sort()
|
||||||
|
}
|
||||||
|
}
|
125
doc/kiln.1.scd
125
doc/kiln.1.scd
|
@ -6,89 +6,125 @@ kiln - a simple static site generator for Gemini sites
|
||||||
|
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
|
|
||||||
_kiln_
|
_kiln_ [--html]
|
||||||
|
|
||||||
|
# OPTIONS
|
||||||
|
|
||||||
|
\--html
|
||||||
|
If this flag is present, kiln will output HTML as well as Gemini text.
|
||||||
|
|
||||||
# SITE STRUCTURE
|
# SITE STRUCTURE
|
||||||
|
|
||||||
A kiln site is organized in the following way:
|
A kiln site is structured in the following way:
|
||||||
|
|
||||||
|[ *Directory*
|
[[ *Directory*
|
||||||
:[ *Description*
|
:[ *Description*
|
||||||
| src/
|
| src/
|
||||||
: Site source
|
: Site source
|
||||||
| templates/
|
| templates/
|
||||||
: Templates
|
: Site templates
|
||||||
| dst/
|
| dst/
|
||||||
: Site destination
|
: Site destination
|
||||||
| dst.html/
|
| html/
|
||||||
: Site HTML destination
|
: Site HTML destination
|
||||||
|
|
||||||
# TEMPLATES
|
# TEMPLATES
|
||||||
|
|
||||||
kiln looks for templates in the "templates" directory.
|
kiln looks for templates in the *templates* directory.
|
||||||
The following templates are accepted:
|
The following templates are supported:
|
||||||
|
|
||||||
|[ *Template*
|
[[ *Template*
|
||||||
:[ *Description*
|
:[ *Description*
|
||||||
| page.gmi
|
| page.gmi
|
||||||
: Page template
|
: Page template
|
||||||
| index.gmi
|
| index.gmi
|
||||||
: Directory index template
|
: Directory index template
|
||||||
|
| feed.gmi
|
||||||
|
: Gemini feed template
|
||||||
| output.html
|
| output.html
|
||||||
: text/gemini to HTML template
|
: HTML output template
|
||||||
|
|
||||||
|
The scope of templates can be limited by placing them in subdirectories of the templates directory.
|
||||||
|
For example, the template templates/blog/page.gmi will apply to all pages in src/blog.
|
||||||
|
|
||||||
|
## FUNCTIONS
|
||||||
|
|
||||||
|
All templates have the following functions available to them:
|
||||||
|
|
||||||
|
[[ *Function*
|
||||||
|
:[ *Description*
|
||||||
|
| site
|
||||||
|
: Returns site metadata
|
||||||
|
|
||||||
|
## SITE METADATA
|
||||||
|
|
||||||
|
Site metadata contains the following information:
|
||||||
|
|
||||||
|
[[ *Variable*
|
||||||
|
:[ *Description*
|
||||||
|
| Title
|
||||||
|
: The title of the site, which can be specified in the site configuration.
|
||||||
|
|
||||||
## PAGE TEMPLATES
|
## PAGE TEMPLATES
|
||||||
|
|
||||||
Page templates are provided with the following information:
|
Page templates are provided with the following information:
|
||||||
|
|
||||||
|[ *Variable*
|
[[ *Variable*
|
||||||
:[ *Description*
|
:[ *Description*
|
||||||
| Title
|
| Title
|
||||||
: The title of the page
|
: The title of the page
|
||||||
| Date
|
| Date
|
||||||
: The date of the page
|
: The date of the page
|
||||||
| Permalink
|
| Path
|
||||||
: Permalink to the page
|
: Path to the page
|
||||||
| Content
|
| Content
|
||||||
: The contents of the page
|
: The contents of the page
|
||||||
|
|
||||||
Pages can specify dates in their filenames.
|
Pages can specify dates in their filenames.
|
||||||
kiln will recognize the date and remove it from the permalink.
|
For example, src/2020-11-20-Hello-world.gmi will have a path of /Hello-world and a date of November 20, 2020.
|
||||||
|
|
||||||
Pages can also specify titles in their content.
|
Pages can specify a title in a top-level heading line.
|
||||||
kiln will parse and remove the title from the content.
|
The heading must be the first line in the page, and can optionally be followed by a blank line.
|
||||||
|
Both lines will be removed from the page content.
|
||||||
|
|
||||||
## INDEX TEMPLATES
|
## INDEX TEMPLATES
|
||||||
|
|
||||||
Index templates are provided with the following information:
|
Index templates are provided with the following information:
|
||||||
|
|
||||||
|[ *Variable*
|
[[ *Variable*
|
||||||
:[ *Description*
|
:[ *Description*
|
||||||
| Permalink
|
| Path
|
||||||
: Permalink to the directory
|
: Path to the directory
|
||||||
| Pages
|
| Pages
|
||||||
: List of pages in this directory
|
: List of pages in this directory
|
||||||
| Directories
|
| Dirs
|
||||||
: List of subdirectories
|
: List of subdirectories
|
||||||
|
|
||||||
# PERMALINKS
|
## FEED TEMPLATES
|
||||||
|
|
||||||
Every page and directory in the site is assigned a permalink.
|
Feed templates are provided with the following information:
|
||||||
Permalinks are absolute and point to the destination file.
|
|
||||||
All files are written to their permalink plus "index.gmi".
|
|
||||||
|
|
||||||
# FRONTMATTER
|
[[ *Variable*
|
||||||
|
|
||||||
Frontmatter uses the _ini_ format.
|
|
||||||
Keys and values are separated by '='.
|
|
||||||
The following keys are supported:
|
|
||||||
|
|
||||||
|[ *Key*
|
|
||||||
:[ *Description*
|
:[ *Description*
|
||||||
| title
|
| Title
|
||||||
: The title of the page
|
: Title of the feed
|
||||||
| date
|
| Path
|
||||||
: The date of the page, specified in the format "2006-01-02".
|
: Path to the feed directory
|
||||||
|
| Entries
|
||||||
|
: List of feed entries
|
||||||
|
|
||||||
|
Feeds are written to the directory path plus "feed".
|
||||||
|
|
||||||
|
## HTML TEMPLATES
|
||||||
|
|
||||||
|
HTML output templates are provided with the following information:
|
||||||
|
|
||||||
|
[[ *Variable*
|
||||||
|
:[ *Description*
|
||||||
|
| Title
|
||||||
|
: Title of the page
|
||||||
|
| Content
|
||||||
|
: HTML contents of the page
|
||||||
|
|
||||||
# CONFIGURATION
|
# CONFIGURATION
|
||||||
|
|
||||||
|
@ -96,19 +132,16 @@ kiln looks for a configuration file named "config.ini".
|
||||||
The configuration file uses the _ini_ format.
|
The configuration file uses the _ini_ format.
|
||||||
The following keys are supported:
|
The following keys are supported:
|
||||||
|
|
||||||
|[ *Key*
|
[[ *Key*
|
||||||
:[ *Description*
|
:[ *Description*
|
||||||
:[ *Default value*
|
|
||||||
| title
|
| title
|
||||||
: Site title
|
: Site title
|
||||||
: ""
|
|
||||||
| url
|
The following sections are supported:
|
||||||
: Site URL
|
|
||||||
: ""
|
[[ *Section*
|
||||||
| atom
|
:[ *Description*
|
||||||
: Output an atom feed
|
| feeds
|
||||||
: false
|
: A list of feeds. Each key denotes a path to a directory, and each value
|
||||||
| html
|
denotes the title of the feed.
|
||||||
: Output HTML
|
|
||||||
: false
|
|
||||||
|
|
||||||
|
|
22
html.go
22
html.go
|
@ -1,27 +1,27 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.sr.ht/~adnano/go-gemini"
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
)
|
)
|
||||||
|
|
||||||
// textToHTML returns the Gemini text response as HTML.
|
// textToHTML returns the Gemini text response as HTML.
|
||||||
func textToHTML(text gemini.Text) string {
|
func textToHTML(text gemini.Text) []byte {
|
||||||
var b strings.Builder
|
var b bytes.Buffer
|
||||||
var pre bool
|
var pre bool
|
||||||
var list bool
|
var list bool
|
||||||
for _, l := range text {
|
for _, l := range text {
|
||||||
if _, ok := l.(gemini.LineListItem); ok {
|
if _, ok := l.(gemini.LineListItem); ok {
|
||||||
if !list {
|
if !list {
|
||||||
list = true
|
list = true
|
||||||
fmt.Fprint(&b, "<ul>\n")
|
b.WriteString("<ul>\n")
|
||||||
}
|
}
|
||||||
} else if list {
|
} else if list {
|
||||||
list = false
|
list = false
|
||||||
fmt.Fprint(&b, "</ul>\n")
|
b.WriteString("</ul>\n")
|
||||||
}
|
}
|
||||||
switch l.(type) {
|
switch l.(type) {
|
||||||
case gemini.LineLink:
|
case gemini.LineLink:
|
||||||
|
@ -35,9 +35,9 @@ func textToHTML(text gemini.Text) string {
|
||||||
case gemini.LinePreformattingToggle:
|
case gemini.LinePreformattingToggle:
|
||||||
pre = !pre
|
pre = !pre
|
||||||
if pre {
|
if pre {
|
||||||
fmt.Fprint(&b, "<pre>\n")
|
b.WriteString("<pre>\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprint(&b, "</pre>\n")
|
b.WriteString("</pre>\n")
|
||||||
}
|
}
|
||||||
case gemini.LinePreformattedText:
|
case gemini.LinePreformattedText:
|
||||||
text := string(l.(gemini.LinePreformattedText))
|
text := string(l.(gemini.LinePreformattedText))
|
||||||
|
@ -60,17 +60,17 @@ func textToHTML(text gemini.Text) string {
|
||||||
case gemini.LineText:
|
case gemini.LineText:
|
||||||
text := string(l.(gemini.LineText))
|
text := string(l.(gemini.LineText))
|
||||||
if text == "" {
|
if text == "" {
|
||||||
fmt.Fprint(&b, "<br>\n")
|
b.WriteString("<br>\n")
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
|
fmt.Fprintf(&b, "<p>%s</p>\n", html.EscapeString(text))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if pre {
|
if pre {
|
||||||
fmt.Fprint(&b, "</pre>\n")
|
b.WriteString("</pre>\n")
|
||||||
}
|
}
|
||||||
if list {
|
if list {
|
||||||
fmt.Fprint(&b, "</ul>\n")
|
b.WriteString("</ul>\n")
|
||||||
}
|
}
|
||||||
return b.String()
|
return b.Bytes()
|
||||||
}
|
}
|
||||||
|
|
478
main.go
478
main.go
|
@ -3,487 +3,93 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
pathpkg "path"
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.sr.ht/~adnano/go-gemini"
|
"git.sr.ht/~adnano/go-gemini"
|
||||||
"git.sr.ht/~adnano/go-ini"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
srcDir = "src" // site source
|
|
||||||
dstDir = "dst" // site destination
|
|
||||||
htmlDst = "dst.html" // html destination
|
|
||||||
indexPath = "index.gmi" // path used for index files
|
|
||||||
atomPath = "atom.xml" // path used for atom feeds
|
|
||||||
tmplDir = "templates" // templates directory
|
|
||||||
atomTmpl = "/atom.xml" // path to atom template
|
|
||||||
indexTmpl = "/index.gmi" // path to index template
|
|
||||||
pageTmpl = "/page.gmi" // path to page template
|
|
||||||
htmlTmpl = "/output.html" // path to html template
|
|
||||||
)
|
|
||||||
|
|
||||||
// site config
|
|
||||||
var cfg struct {
|
|
||||||
title string // site title
|
|
||||||
url string // site URL
|
|
||||||
toAtom bool // output Atom
|
|
||||||
toHTML bool // output HTML
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Try to read config file
|
|
||||||
if f, err := os.Open("config.ini"); err == nil {
|
|
||||||
ini.Parse(f, func(section, key, value string) {
|
|
||||||
if section == "" {
|
|
||||||
switch key {
|
|
||||||
case "title":
|
|
||||||
cfg.title = value
|
|
||||||
case "url":
|
|
||||||
cfg.url = value
|
|
||||||
case "atom":
|
|
||||||
b, err := strconv.ParseBool(value)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cfg.toAtom = b
|
|
||||||
case "html":
|
|
||||||
b, err := strconv.ParseBool(value)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cfg.toHTML = b
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
flag.Parse()
|
|
||||||
if err := run(); err != nil {
|
if err := run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// site metadata passed to templates
|
|
||||||
type site struct {
|
|
||||||
Title string
|
|
||||||
URL string
|
|
||||||
}
|
|
||||||
|
|
||||||
// map of paths to templates
|
|
||||||
var templates = map[string]*template.Template{}
|
|
||||||
|
|
||||||
// template functions
|
|
||||||
var funcMap = template.FuncMap{
|
|
||||||
"site": func() site {
|
|
||||||
return site{
|
|
||||||
Title: cfg.title,
|
|
||||||
URL: cfg.url,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// loadTemplate loads a template from the provided path and content.
|
|
||||||
func loadTemplate(path string, content string) {
|
|
||||||
tmpl := template.New(path)
|
|
||||||
tmpl.Funcs(funcMap)
|
|
||||||
template.Must(tmpl.Parse(content))
|
|
||||||
templates[path] = tmpl
|
|
||||||
}
|
|
||||||
|
|
||||||
// findTemplate searches recursively for a template for the given path.
|
|
||||||
func findTemplate(path string, tmpl string) *template.Template {
|
|
||||||
for {
|
|
||||||
tmplPath := filepath.Join(path, tmpl)
|
|
||||||
if t, ok := templates[tmplPath]; ok {
|
|
||||||
return t
|
|
||||||
}
|
|
||||||
slash := path == "/"
|
|
||||||
path = filepath.Dir(path)
|
|
||||||
if slash && path == "/" {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// shouldn't happen
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
// Load default templates
|
// whether or not to output HTML
|
||||||
loadTemplate(atomTmpl, atom_xml)
|
var toHTML bool
|
||||||
loadTemplate(indexTmpl, index_gmi)
|
flag.BoolVar(&toHTML, "html", false, "output HTML")
|
||||||
loadTemplate(pageTmpl, page_gmi)
|
flag.Parse()
|
||||||
loadTemplate(htmlTmpl, output_html)
|
|
||||||
// Load templates
|
// Load config
|
||||||
filepath.Walk(tmplDir, func(path string, info os.FileInfo, err error) error {
|
cfg := NewConfig()
|
||||||
if err != nil {
|
if err := cfg.Load("config.ini"); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if info.Mode().IsRegular() {
|
if err := cfg.LoadTemplates("templates"); err != nil {
|
||||||
b, err := ioutil.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Remove "templates/" from beginning of path
|
|
||||||
path = strings.TrimPrefix(path, tmplDir)
|
|
||||||
loadTemplate(path, string(b))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
// Load content
|
// Load content
|
||||||
dir := NewDir("")
|
dir := NewDir("")
|
||||||
if err := dir.read(srcDir, ""); err != nil {
|
if err := dir.read("src", ""); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Sort content
|
|
||||||
dir.sort()
|
dir.sort()
|
||||||
// Manipulate content
|
// Manipulate content
|
||||||
if err := manipulate(dir); err != nil {
|
if err := dir.manipulate(cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cfg.toAtom {
|
|
||||||
if err := createFeeds(dir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Write content
|
// Write content
|
||||||
if err := write(dir, dstDir, outputGemini); err != nil {
|
if err := dir.write("dst", outputGemini, cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if cfg.toHTML {
|
if toHTML {
|
||||||
if err := write(dir, htmlDst, outputHTML); err != nil {
|
if err := dir.write("html", outputHTML, cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// write writes the contents of the Index to the provided destination directory.
|
|
||||||
func write(dir *Dir, dstDir string, format outputFormat) error {
|
|
||||||
// Empty the destination directory
|
|
||||||
if err := os.RemoveAll(dstDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Create the destination directory
|
|
||||||
if err := os.MkdirAll(dstDir, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Write the directory
|
|
||||||
return dir.write(dstDir, format)
|
|
||||||
}
|
|
||||||
|
|
||||||
// manipulate processes and manipulates the site's content.
|
|
||||||
func manipulate(dir *Dir) error {
|
|
||||||
// Write the directory index file, if it doesn't exist
|
|
||||||
if dir.index == nil {
|
|
||||||
var b bytes.Buffer
|
|
||||||
tmpl := findTemplate(dir.Permalink, indexTmpl)
|
|
||||||
if err := tmpl.Execute(&b, dir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dir.index = &Page{
|
|
||||||
Permalink: dir.Permalink,
|
|
||||||
content: b.Bytes(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Manipulate pages
|
|
||||||
for i := range dir.Pages {
|
|
||||||
var b bytes.Buffer
|
|
||||||
tmpl := findTemplate(dir.Pages[i].Permalink, pageTmpl)
|
|
||||||
if err := tmpl.Execute(&b, dir.Pages[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
dir.Pages[i].content = b.Bytes()
|
|
||||||
}
|
|
||||||
// Manipulate subdirectories
|
|
||||||
for _, d := range dir.Dirs {
|
|
||||||
if err := manipulate(d); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// feed represents an Atom feed.
|
|
||||||
type feed struct {
|
|
||||||
Title string // Feed title.
|
|
||||||
Path string // Feed path.
|
|
||||||
URL string // Site URL.
|
|
||||||
Updated time.Time // Last updated time.
|
|
||||||
Entries []*Page // Feed entries.
|
|
||||||
}
|
|
||||||
|
|
||||||
// createFeeds creates Atom feeds.
|
|
||||||
func createFeeds(dir *Dir) error {
|
|
||||||
var b bytes.Buffer
|
|
||||||
tmpl := templates[atomTmpl]
|
|
||||||
feed := &feed{
|
|
||||||
Title: cfg.title,
|
|
||||||
Path: filepath.Join(dir.Permalink, atomPath),
|
|
||||||
URL: cfg.url,
|
|
||||||
Updated: time.Now(),
|
|
||||||
Entries: dir.Pages,
|
|
||||||
}
|
|
||||||
if err := tmpl.Execute(&b, feed); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
path := filepath.Join(dir.Permalink, atomPath)
|
|
||||||
dir.Files[path] = b.Bytes()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page represents a page.
|
|
||||||
type Page struct {
|
|
||||||
Title string // The title of this page.
|
|
||||||
Permalink string // The permalink to this page.
|
|
||||||
Date time.Time // The date of the page.
|
|
||||||
content []byte // The content of this page.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content returns the page content as a string.
|
|
||||||
// Used in templates.
|
|
||||||
func (p *Page) Content() string {
|
|
||||||
return string(p.content)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regexp to parse title from Gemini files
|
|
||||||
var titleRE = regexp.MustCompile("^# ?([^#\r\n]+)\r?\n?\r?\n?")
|
|
||||||
|
|
||||||
// Regexp to parse frontmatter from Gemini files
|
|
||||||
var frontmatterRE = regexp.MustCompile("---\r?\n(?s)(.*)(?-s)---\r?\n?")
|
|
||||||
|
|
||||||
// NewPage returns a new Page with the given path and content.
|
|
||||||
func NewPage(path string, content []byte) *Page {
|
|
||||||
var page Page
|
|
||||||
|
|
||||||
// Try to parse frontmatter
|
|
||||||
if submatches := frontmatterRE.FindSubmatch(content); submatches != nil {
|
|
||||||
ini.Parse(bytes.NewReader(submatches[1]), func(section, key, value string) {
|
|
||||||
if section == "" {
|
|
||||||
switch key {
|
|
||||||
case "title":
|
|
||||||
page.Title = value
|
|
||||||
case "date":
|
|
||||||
date, err := time.Parse("2006-01-02", value)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
page.Date = date
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Preserve unrecognized keys
|
|
||||||
|
|
||||||
})
|
|
||||||
fmt.Println(string(submatches[1]))
|
|
||||||
content = content[len(submatches[0]):]
|
|
||||||
} else {
|
|
||||||
// Try to parse the date from the page filename
|
|
||||||
const layout = "2006-01-02"
|
|
||||||
base := filepath.Base(path)
|
|
||||||
if len(base) >= len(layout) {
|
|
||||||
dateStr := base[:len(layout)]
|
|
||||||
if time, err := time.Parse(layout, dateStr); err == nil {
|
|
||||||
page.Date = time
|
|
||||||
}
|
|
||||||
// Remove the date from the path
|
|
||||||
base = base[len(layout):]
|
|
||||||
if len(base) > 0 {
|
|
||||||
// Remove a leading dash
|
|
||||||
if base[0] == '-' {
|
|
||||||
base = base[1:]
|
|
||||||
}
|
|
||||||
if len(base) > 0 {
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
if dir == "." {
|
|
||||||
dir = ""
|
|
||||||
}
|
|
||||||
path = filepath.Join(dir, base)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the title from the contents
|
|
||||||
if submatches := titleRE.FindSubmatch(content); submatches != nil {
|
|
||||||
page.Title = string(submatches[1])
|
|
||||||
// Remove the title from the contents
|
|
||||||
content = content[len(submatches[0]):]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
page.Permalink = "/" + strings.TrimSuffix(path, ".gmi") + "/"
|
|
||||||
page.content = content
|
|
||||||
return &page
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dir represents a directory.
|
|
||||||
type Dir struct {
|
|
||||||
Permalink string // Permalink to this directory.
|
|
||||||
Pages []*Page // Pages in this directory.
|
|
||||||
Dirs []*Dir // Subdirectories.
|
|
||||||
Files map[string][]byte // Static files.
|
|
||||||
index *Page // The index file (index.gmi).
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewDir returns a new Dir with the given path.
|
|
||||||
func NewDir(path string) *Dir {
|
|
||||||
var permalink string
|
|
||||||
if path == "" {
|
|
||||||
permalink = "/"
|
|
||||||
} else {
|
|
||||||
permalink = "/" + path + "/"
|
|
||||||
}
|
|
||||||
return &Dir{
|
|
||||||
Permalink: permalink,
|
|
||||||
Files: map[string][]byte{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// read reads from a directory and indexes the files and directories within it.
|
|
||||||
func (d *Dir) read(srcDir string, path string) error {
|
|
||||||
entries, err := ioutil.ReadDir(filepath.Join(srcDir, path))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
name := entry.Name()
|
|
||||||
// Ignore names that start with "_"
|
|
||||||
if strings.HasPrefix(name, "_") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := filepath.Join(path, name)
|
|
||||||
if entry.IsDir() {
|
|
||||||
// Gather directory data
|
|
||||||
dir := NewDir(path)
|
|
||||||
if err := dir.read(srcDir, path); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
d.Dirs = append(d.Dirs, dir)
|
|
||||||
} else {
|
|
||||||
srcPath := filepath.Join(srcDir, path)
|
|
||||||
content, err := ioutil.ReadFile(srcPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if filepath.Ext(name) == ".gmi" {
|
|
||||||
// Gather page data
|
|
||||||
page := NewPage(path, content)
|
|
||||||
if name == "index.gmi" {
|
|
||||||
d.index = page
|
|
||||||
} else {
|
|
||||||
d.Pages = append(d.Pages, page)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Static file
|
|
||||||
d.Files[path] = content
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// write writes the Dir to the provided destination path.
|
|
||||||
func (d *Dir) write(dstDir string, format outputFormat) error {
|
|
||||||
// Create the directory
|
|
||||||
dirPath := filepath.Join(dstDir, d.Permalink)
|
|
||||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write static files
|
|
||||||
for path := range d.Files {
|
|
||||||
dstPath := filepath.Join(dstDir, path)
|
|
||||||
f, err := os.Create(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
data := d.Files[path]
|
|
||||||
if _, err := f.Write(data); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the pages
|
|
||||||
for _, page := range d.Pages {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
if _, err := f.Write(content); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the index file
|
|
||||||
if d.index != nil {
|
|
||||||
path, content := format(d.index)
|
|
||||||
dstPath := filepath.Join(dstDir, path)
|
|
||||||
f, err := os.Create(dstPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := f.Write(content); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write subdirectories
|
|
||||||
for _, dir := range d.Dirs {
|
|
||||||
dir.write(dstDir, format)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// sort sorts the directory's pages by date.
|
|
||||||
func (d *Dir) sort() {
|
|
||||||
sort.Slice(d.Pages, func(i, j int) bool {
|
|
||||||
return d.Pages[i].Date.After(d.Pages[j].Date)
|
|
||||||
})
|
|
||||||
// Sort subdirectories
|
|
||||||
for _, d := range d.Dirs {
|
|
||||||
d.sort()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// outputFormat represents an output format.
|
// outputFormat represents an output format.
|
||||||
type outputFormat func(*Page) (path string, content []byte)
|
type outputFormat func(*Page, *Config) (path string, content []byte)
|
||||||
|
|
||||||
func outputGemini(p *Page) (path string, content []byte) {
|
// outputGemini outputs the page as Gemini text.
|
||||||
path = filepath.Join(p.Permalink, indexPath)
|
func outputGemini(p *Page, cfg *Config) (path string, content []byte) {
|
||||||
content = p.content
|
path = p.Path
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
path += "index.gmi"
|
||||||
|
} else {
|
||||||
|
path += "/index.gmi"
|
||||||
|
}
|
||||||
|
content = []byte(p.Content)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func outputHTML(p *Page) (path string, content []byte) {
|
// outputHTML outputs the page as HTML.
|
||||||
const indexPath = "index.html"
|
func outputHTML(p *Page, cfg *Config) (path string, content []byte) {
|
||||||
path = filepath.Join(p.Permalink, indexPath)
|
path = p.Path
|
||||||
|
if strings.HasSuffix(path, "/") {
|
||||||
|
path += "index.html"
|
||||||
|
} else {
|
||||||
|
path += "/index.html"
|
||||||
|
}
|
||||||
|
|
||||||
r := bytes.NewReader(p.content)
|
r := strings.NewReader(p.Content)
|
||||||
text := gemini.ParseText(r)
|
text := gemini.ParseText(r)
|
||||||
content = []byte(textToHTML(text))
|
content = textToHTML(text)
|
||||||
|
|
||||||
type tmplCtx struct {
|
// html template context
|
||||||
Title string
|
type htmlCtx struct {
|
||||||
Content string
|
Title string // page title
|
||||||
|
Content string // page HTML contents
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
tmpl := templates[htmlTmpl]
|
tmpl := cfg.Templates.FindTemplate(pathpkg.Dir(path), "output.html")
|
||||||
tmpl.Execute(&b, &tmplCtx{
|
tmpl.Execute(&b, &htmlCtx{
|
||||||
Title: p.Title,
|
Title: p.Title,
|
||||||
Content: string(content),
|
Content: string(content),
|
||||||
})
|
})
|
||||||
|
|
62
page.go
Normal file
62
page.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
pathpkg "path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Page represents a page.
|
||||||
|
type Page struct {
|
||||||
|
Title string // The title of this page.
|
||||||
|
Path string // The path to this page.
|
||||||
|
Date time.Time // The date of the page.
|
||||||
|
Content string // The content of this page.
|
||||||
|
}
|
||||||
|
|
||||||
|
// titleRe is a regexp to parse titles from Gemini files.
|
||||||
|
// It also matches the next line if it is empty.
|
||||||
|
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 []byte) *Page {
|
||||||
|
var page Page
|
||||||
|
|
||||||
|
// Try to parse the date from the page filename
|
||||||
|
const layout = "2006-01-02"
|
||||||
|
base := pathpkg.Base(path)
|
||||||
|
if len(base) >= len(layout) {
|
||||||
|
dateStr := base[:len(layout)]
|
||||||
|
if time, err := time.Parse(layout, dateStr); err == nil {
|
||||||
|
page.Date = time
|
||||||
|
}
|
||||||
|
// Remove the date from the path
|
||||||
|
base = base[len(layout):]
|
||||||
|
if len(base) > 0 {
|
||||||
|
// Remove a leading dash
|
||||||
|
if base[0] == '-' {
|
||||||
|
base = base[1:]
|
||||||
|
}
|
||||||
|
if len(base) > 0 {
|
||||||
|
dir := pathpkg.Dir(path)
|
||||||
|
if dir == "." {
|
||||||
|
dir = ""
|
||||||
|
}
|
||||||
|
path = pathpkg.Join(dir, base)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse the title from the contents
|
||||||
|
if submatches := titleRE.FindSubmatch(content); submatches != nil {
|
||||||
|
page.Title = string(submatches[1])
|
||||||
|
// Remove the title from the contents
|
||||||
|
content = content[len(submatches[0]):]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove extension from path
|
||||||
|
page.Path = "/" + strings.TrimSuffix(path, ".gmi")
|
||||||
|
page.Content = string(content)
|
||||||
|
return &page
|
||||||
|
}
|
113
templates.go
113
templates.go
|
@ -1,33 +1,86 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
// TODO: Use go:embed
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
)
|
||||||
|
|
||||||
// Default atom feed template
|
// Templates contains site templates.
|
||||||
const atom_xml = `<?xml version="1.0" encoding="utf-8"?>
|
type Templates struct {
|
||||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
tmpls map[string]*template.Template
|
||||||
<title>{{ .Title }}</title>
|
funcs template.FuncMap
|
||||||
<link href="{{ .URL }}"/>
|
}
|
||||||
<link rel="self" href="{{ .URL }}{{ .Path }}"/>
|
|
||||||
<updated>{{ .Updated.Format "2006-01-02T15:04:05Z07:00" }}</updated>
|
// NewTemplates returns a new Templates with the default templates.
|
||||||
<id>{{ .URL }}{{ .Path }}</id>
|
func NewTemplates() *Templates {
|
||||||
{{- $url := .URL -}}
|
t := &Templates{
|
||||||
{{- range .Entries }}
|
tmpls: map[string]*template.Template{},
|
||||||
<entry>
|
}
|
||||||
<title>{{ .Title }}</title>
|
// Load default templates
|
||||||
<link href="{{ $url }}{{ .Permalink }}"/>
|
t.LoadTemplate("/index.gmi", index_gmi)
|
||||||
<id>{{ $url }}{{ .Permalink }}</id>
|
t.LoadTemplate("/page.gmi", page_gmi)
|
||||||
<updated>{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}</updated>
|
t.LoadTemplate("/feed.gmi", feed_gmi)
|
||||||
<content src="{{ $url }}{{ .Permalink }}" type="text/gemini"></content>
|
t.LoadTemplate("/output.html", output_html)
|
||||||
</entry>
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Funcs sets the functions available to newly created templates.
|
||||||
|
func (t *Templates) Funcs(funcs template.FuncMap) {
|
||||||
|
t.funcs = funcs
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadTemplate loads a template from the provided path and content.
|
||||||
|
func (t *Templates) LoadTemplate(path string, content string) {
|
||||||
|
tmpl := template.New(path)
|
||||||
|
tmpl.Funcs(t.funcs)
|
||||||
|
template.Must(tmpl.Parse(content))
|
||||||
|
t.tmpls[path] = tmpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads templates from the provided directory
|
||||||
|
func (t *Templates) Load(dir string) error {
|
||||||
|
return filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if info.Mode().IsRegular() {
|
||||||
|
b, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Remove directory from beginning of path
|
||||||
|
path = strings.TrimPrefix(path, dir)
|
||||||
|
t.LoadTemplate(path, string(b))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindTemplate searches recursively for a template for the given path.
|
||||||
|
func (t *Templates) FindTemplate(path string, tmpl string) *template.Template {
|
||||||
|
for {
|
||||||
|
tmplPath := filepath.Join(path, tmpl)
|
||||||
|
if t, ok := t.tmpls[tmplPath]; ok {
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
slash := path == "/"
|
||||||
|
path = filepath.Dir(path)
|
||||||
|
if slash && path == "/" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default index template
|
||||||
|
const index_gmi = `# Index of {{ .Path }}
|
||||||
|
|
||||||
|
{{ range .Dirs }}=> {{ .Path }}
|
||||||
{{ end -}}
|
{{ end -}}
|
||||||
</feed>`
|
{{ range .Pages }}=> {{ .Path }}
|
||||||
|
|
||||||
// Default directory index template
|
|
||||||
const index_gmi = `# Index of {{ .Permalink }}
|
|
||||||
|
|
||||||
{{ range .Dirs }}=> {{ .Permalink }}
|
|
||||||
{{ end -}}
|
|
||||||
{{ range .Pages }}=> {{ .Permalink }}
|
|
||||||
{{ end -}}`
|
{{ end -}}`
|
||||||
|
|
||||||
// Default page template
|
// Default page template
|
||||||
|
@ -35,6 +88,14 @@ const page_gmi = `# {{ .Title }}
|
||||||
|
|
||||||
{{ .Content }}`
|
{{ .Content }}`
|
||||||
|
|
||||||
|
// Default feed template
|
||||||
|
const feed_gmi = `# {{ .Title }}
|
||||||
|
|
||||||
|
Last updated at {{ .Updated.Format "2006-01-02" }}
|
||||||
|
|
||||||
|
{{ range .Entries }}=> {{ .Path }} {{ .Date.Format "2006-01-02" }} {{ .Title }}
|
||||||
|
{{ end -}}`
|
||||||
|
|
||||||
// Default template for html output
|
// Default template for html output
|
||||||
const output_html = `<!DOCTYPE html>
|
const output_html = `<!DOCTYPE html>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
|
|
Loading…
Reference in a new issue