Compare commits

..

10 commits
dev ... main

47 changed files with 44 additions and 2114 deletions

View file

@ -14,4 +14,3 @@ tasks:
git remote add github "$github"
git push --mirror github

View file

@ -1,49 +1,65 @@
# Zona
Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles.
**IMPORTANT:** Zona is currently migrating to a Python implementation. The new
repository is called [zona-py](https://git.sr.ht/~ficd/zona-py). **It will
replace this repository once the migration is complete.**
**Warning:** Zona has not yet reached **v1**. The `dev-*` branches of this repository contain the code -- however, there is no assurance of stability or functionality until the first release. Configuration and usage documentation will also be provided at this time.
[Zona](https://sr.ht/~ficd/zona/) is a tool for building a static website,
optimized for lightweight blogs following minimalist design principles. The
project is hosted on [sourcehut](https://sr.ht/~ficd/zona/) and mirrored on
[GitHub](https://github.com/ficcdaf/zona). I am not accepting GitHub issues,
please make your submission to the
[issue tracker](https://todo.sr.ht/~ficd/zona) or send an email to the public
mailing list at
[~ficd/zona-devel@lists.sr.ht](mailto:~ficd/zona-devel@lists.sr.ht)
## Table of Contents
<!-- prettier-ignore-start -->
- [Design Goals](#design-goals)
- [v1 Features](#v1-features)
- [Roadmap](#roadmap)
- [Contribution](#contribution)
- [Inspirations](#inspirations)
> Zona is currently in development. The `main` branch of this repository does
> not yet contain the software. The `dev-stable` branch contains the code used
> to generate [ficd.ca](https://ficd.ca) -- although the program is undocumented
> and missing features, so please proceed at your own risk. The `dev` branch
> contains the latest development updates and is not guaranteed to be functional
> (or even compile) at any given commit. Kindly note that the commit history
> will be cleaned up before the program is merged into the `main` branch.
<!-- prettier-ignore-end -->
## Design Goals
Zona is intended to be easy-to-use. A user should be able to build a reasonably complex website or blog with only a directory of Markdown content and a single command, without needing to write any HTML or configuration. However, users should optionally have access to sensible and flexible configuration options, including writing HTML. The output of Zona should also be lightweight, retaining the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages.
Zona is intended to be easy-to-use. A user should be able to build a reasonably
complex website or blog with only a directory of Markdown content and a single
command, without needing to write any HTML or configuration. However, users
should optionally have access to sensible and flexible configuration options,
including writing HTML. The output of Zona should also be lightweight, retaining
the smallest file sizes possible. These characteristics make Zona well-suited
for both beginners and power users that wish to host a website on a service like
Neocities or GitHub Pages.
## v1 Features
## Features Implemented
- Write pages purely in Markdown.
- Single-command build process.
- Lightweight output.
- Sensible default template and stylesheet.
- Configuration entirely optional, but very powerful.
- Configuration file.
- Internal links preserved.
- Custom image element parsing & formatting.
- Site header and footer defined in Markdown.
- Declarative metadata per Markdown file.
- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements.
- YAML frontmatter support.
## Planned Features
- Automatically treat contents of `posts/` directory as blog posts.
- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery`
elements.
- Support for custom stylesheets, favicons, and page templates.
## Roadmap
- [ ] RSS/Atom feed generation.
- [ ] Image optimization & dithering.
- [ ] Windows, Mac, Linux releases.
- [ ] AUR package.
- [ ] Custom Markdown tags that expand to user-defined templates.
- [ ] Live preview local server.
## Contribution
Zona is a small project maintained by a very busy graduate student. If you want to contribute, you are more than welcome to submit issues and pull requests.
- Image optimization and dithering.
- Custom markdown tags that expand to user-defined templates.
- Live preview server.
- Robust tests.
## Inspirations
- [Zoner](https://git.sr.ht/~ryantrawick/zoner)
- [Zonelets](https://zonelets.net/)
> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I have never used it, nor read its documentation, thus it is not listed as an inspiration.

View file

@ -1,106 +0,0 @@
# Zona
Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles.
**Warning:** Zona has not yet reached **v1**. The `dev-*` branches of this repository contain the code -- however, there is no assurance of stability or functionality until the first release. Configuration and usage documentation will also be provided at this time.
## Table of Contents
- [Features](#v1-features)
- [Installation](#installation)
- [Roadmap](#roadmap)
## v1 Features
- Write your pages in Markdown.
- Build a lightweight website with zero JavaScript.
- Simple CLI build interface.
- HTML layout optimized for screen readers and Neocities.
## Getting Started
### Dependencies
- `go 1.23.2`
```Bash
# On Arch Linux
sudo pacman -S go
# On Ubuntu/Debian
sudo apt install go
```
### Installation
First, download the repository and open it:
```Bash
git clone https://github.com/ficcdaf/zona.git && cd zona
```
On Linux:
```Bash
# run the provided build script
./build.sh
```
On other platforms:
```Bash
go build -o bin/zona cmd/zona
```
The resulting binary can be found at `bin/zona`.
## Roadmap
- [ ] Zona configuration file to define build options.
- [ ] Image optimization & dithering options.
- [ ] AUR package after first release
- [ ] Automatic RSS/Atom feed generation.
=======
- [Contribution](#contribution)
- [Inspirations](#inspirations)
## Design Goals
Zona is intended to be easy-to-use. A user should be able to build a reasonably complex website or blog with only a directory of Markdown content and a single command, without needing to write any HTML or configuration. However, users should optionally have access to sensible and flexible configuration options, including writing HTML. The output of Zona should also be lightweight, retaining the smallest file sizes possible. These characteristics make Zona well-suited for both beginners and power users that wish to host a website on a service like Neocities or GitHub Pages.
## v1 Features
- Write pages purely in Markdown.
- Single-command build process.
- Lightweight output.
- Sensible default template and stylesheet.
- Configuration entirely optional, but very powerful.
- Site header and footer defined in Markdown.
- Declarative metadata per Markdown file.
- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements.
- Support for custom stylesheets, favicons, and page templates.
## Roadmap
- [ ] RSS/Atom feed generation.
- [ ] Image optimization & dithering.
- [ ] Windows, Mac, Linux releases.
- [ ] AUR package.
- [ ] Custom Markdown tags that expand to user-defined templates.
- [ ] Live preview local server.
## Contribution
Zona is a small project maintained by a very busy graduate student. If you want to contribute, you are more than welcome to submit issues and pull requests.
>>>>>>> e368126 (Update readme)
## Inspirations
- [Zoner](https://git.sr.ht/~ryantrawick/zoner)
<<<<<<< HEAD
- Zonelets
=======
- [Zonelets](https://zonelets.net/)
> Note: I am aware of `Zola`, and the similar name is entirely a coincidence. I have never used it, nor read its documentation, thus it is not listed as an inspiration.
>>>>>>> e368126 (Update readme)

52
TODO.md
View file

@ -1,52 +0,0 @@
# TO-DO
- **First**, re-write the settings & configuration system from scratch! It's
broken and messy and not worth trying to fix like this. Instead, actually
architect how it should work, _then_ implement it.
- Refactor the directory structure processing
- Implement zola-style structure instead
- `zona init` command to populate the required files, _with_ defaults
(unlike zola)
- Interactive for setting values, also an option to create `.gitignore`
with `public` in it.
- `zona.yml` is **required** and should mark the root:
- `templates`, `content`, `static`, `zona.yml`
- multiple `zona.yml` files should be an error
- if the folder containing `zona.yml` doesn't contain _exactly_ the
expected directories and files, it's an error
- Paths in page metadata should start at these folders
- i.e. `(template|footer|header): name.html``root/templates/name.html`
- `(style|icon): name.ext``root/static/name.ext`
- Traverse `content` and `static` separately, applying different rules
- everything in `static/**` should be directly copied
- `content/**` should be processed
- `*.md` converted, everything else copied directly
- `./name.md` → ./name/index.html
- Either `./name.md` or `./name/index.md` are valid, _together_ they
cause an error!
- What about markdown links to internal pages?
- Relative links should be supported to play nice with LSP
- in case of relative link, zona should attempt to resolve it, figuring
out which file it's pointing to, and convert it to a `/` prefixed link
pointing to appropriate place
- so `../blog/a-post.md``/blog/a-post` where `/blog/a-post/index.html`
exists
- links from project root should also be supported
- check link validity at build time and supply warning
- _tl;dr_ all links should be resolved to the absolute path to that resource
starting from the website root. that's the link that should actually be
written to the HTML.
- Re-consider what `zona.yml` should have in it.
- Set syntax highlighting theme here
- a string that's not a file path: name of any built-in theme in
[chroma](https://github.com/alecthomas/chroma)
- path to `xml` _or_ `yml` file: custom theme for passing to chroma
- if `xml`, pass directly
- if `yml`, parse and convert into expected `xml` format before passing
- Set website root URL here
- toggle option for zona's custom image label expansion, image container div,
etc, basically all the custom rendering stuff
- Syntax highlighting for code blocks
- Add `zona serve` command with local dev server to preview the site
- Both `zona build` and `zona serve` should output warning and error
- Write actual unit tests!

View file

@ -1,59 +0,0 @@
package main
import (
"errors"
"flag"
"fmt"
"os"
"github.com/ficcdaf/zona/internal/builder"
)
// // validateFile checks whether a given path
// // is a valid file && matches an expected extension
// func validateFile(path, ext string) bool {
// return (util.CheckExtension(path, ext) == nil) && (util.PathIsValid(path, true))
// }
func main() {
rootPath := flag.String("file", "", "Path to the markdown file.")
flag.Parse()
if *rootPath == "" {
// no flag provided, check for positional argument instead
n := flag.NArg()
var e error
switch n {
case 1:
// we read the positional arg
arg := flag.Arg(0)
// mdPath wants a pointer so we get arg's address
rootPath = &arg
case 0:
// in case of no flag and no arg, we fail
e = errors.New("Required argument missing!")
default:
// more args than expected is also fail
e = errors.New("Unexpected arguments!")
}
if e != nil {
fmt.Printf("Error: %s\n", e.Error())
os.Exit(1)
}
}
settings := builder.GetSettings(*rootPath, "foobar")
// err := builder.Traverse(*rootPath, "foobar", settings)
// traverse the source and process file metadata
pm, err := builder.ProcessTraverse(*rootPath, "foobar", settings)
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
os.Exit(1)
}
err = builder.BuildProcessedFiles(pm, settings)
if err != nil {
fmt.Printf("Error: %s\n", err.Error())
os.Exit(1)
}
fmt.Printf("%#v", pm)
}

10
go.mod
View file

@ -1,10 +0,0 @@
module github.com/ficcdaf/zona
go 1.24.2
require (
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/text v0.23.0

View file

@ -1,5 +0,0 @@
#!/bin/bash
go mod tidy
go build -o bin/zona ./cmd/zona
sudo cp -f bin/zona /usr/bin/zona

View file

@ -1,203 +0,0 @@
package builder
import (
"bytes"
"errors"
"fmt"
"html/template"
"log"
"path/filepath"
"strings"
"github.com/ficcdaf/zona/internal/util"
"gopkg.in/yaml.v3"
)
type PageData struct {
Title string
Icon string
Stylesheet string
HeaderName string
Header template.HTML
Content template.HTML
NextPost string
PrevPost string
FooterName string
Footer template.HTML
Template string
Type string
}
type Metadata map[string]any
type FrontMatter struct {
Title string `yaml:"title"`
Icon string `yaml:"icon"`
Style string `yaml:"style"`
Header string `yaml:"header"`
Footer string `yaml:"footer"`
Type string `yaml:"type"`
}
func processWithYaml(f []byte) (*FrontMatter, []byte, error) {
// Check if the file has valid metadata
trimmed := bytes.TrimSpace(f)
normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n")
if !strings.HasPrefix(normalized, ("---\n")) {
// No valid yaml, so return the entire content
return nil, f, nil
}
// Separate YAML from rest of document
split := strings.SplitN(normalized, "---\n", 3)
if len(split) < 3 {
return nil, nil, fmt.Errorf("invalid frontmatter format")
}
var meta FrontMatter
// Parse YAML
if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil {
return nil, nil, err
}
return &meta, []byte(split[2]), nil
}
func buildPageData(m *FrontMatter, in string, out string, settings *Settings) *PageData {
p := &PageData{}
if m != nil && m.Title != "" {
p.Title = util.WordsToTitle(m.Title)
} else {
p.Title = util.PathToTitle(in)
}
if m != nil && m.Icon != "" {
i, err := util.NormalizePath(m.Icon, in)
if err != nil {
p.Icon = settings.IconName
} else {
p.Icon = i
}
} else {
p.Icon = settings.IconName
}
var stylePath string
if m != nil && m.Style != "" {
stylePath = m.Style
} else {
stylePath = settings.StylePath
}
curDir := filepath.Dir(out)
relPath, err := filepath.Rel(curDir, stylePath)
// fmt.Printf("fp: %s, sp: %s, rp: %s\n", curDir, stylePath, relPath)
if err != nil {
log.Fatalln("Error calculating stylesheet path: ", err)
}
p.Stylesheet = relPath
if m != nil && m.Header != "" {
p.HeaderName = m.Header
// for now we use default anyways
p.Header = settings.Header
} else {
p.HeaderName = settings.HeaderName
p.Header = settings.Header
}
if m != nil && m.Footer != "" {
p.FooterName = m.Footer
p.Footer = settings.Footer
} else {
p.FooterName = settings.FooterName
p.Footer = settings.Footer
}
// TODO: Don't hard code posts dir name
if (m != nil && (m.Type == "article" || m.Type == "post")) || util.InDir(in, "posts") {
p.Template = (settings.ArticleTemplate)
p.Type = "post"
} else {
p.Template = (settings.DefaultTemplate)
p.Type = ""
}
return p
}
func _BuildHtmlFile(in string, out string, settings *Settings) error {
mdPre, err := util.ReadFile(in)
if err != nil {
return err
}
metadata, md, err := processWithYaml(mdPre)
if err != nil {
return err
}
pd := buildPageData(metadata, in, out, settings)
fmt.Println("Title: ", pd.Title)
// build according to template here
html := MdToHTML(md)
pd.Content = template.HTML(html)
tmpl, err := template.New("webpage").Parse(pd.Template)
if err != nil {
return err
}
var output bytes.Buffer
if err := tmpl.Execute(&output, pd); err != nil {
return err
}
err = util.WriteFile(output.Bytes(), out)
return err
}
func BuildFile(f *File, settings *Settings) error {
if f.ShouldCopy {
if err := util.CreateParents(f.OutPath); err != nil {
return err
}
if err := util.CopyFile(f.InPath, f.OutPath); err != nil {
return errors.Join(errors.New("Error processing file "+f.InPath), err)
} else {
return nil
}
}
if err := util.CreateParents(f.OutPath); err != nil {
return err
}
if err := BuildHtmlFile(f.FrontMatterLen, f.InPath, f.OutPath, f.PageData, settings); err != nil {
return errors.Join(errors.New("Error processing file "+f.InPath), err)
} else {
return nil
}
}
func BuildHtmlFile(l int, in string, out string, pd *PageData, settings *Settings) error {
// WARN: ReadLineRange is fine, but l is the len of the frontmatter
// NOT including the delimiters!
start := l
// if the frontmatter exists (len > 0), then we need to
// account for two lines of delimiter!
if l != 0 {
start += 2
}
md, err := util.ReadLineRange(in, start, -1)
if err != nil {
return err
}
fmt.Println("Title: ", pd.Title)
// build according to template here
html := MdToHTML(md)
pd.Content = template.HTML(html)
tmpl, err := template.New("webpage").Parse(pd.Template)
if err != nil {
return err
}
var output bytes.Buffer
if err := tmpl.Execute(&output, pd); err != nil {
return err
}
err = util.WriteFile(output.Bytes(), out)
return err
}

View file

@ -1,155 +0,0 @@
package builder
import (
"embed"
"html/template"
"log"
"path/filepath"
"github.com/ficcdaf/zona/internal/util"
"gopkg.in/yaml.v3"
)
var defaultNames = map[string]string{
"config": ".zona.yml",
"header": "header.md",
"footer": "footer.md",
"style": "style.css",
"stylePath": filepath.Join("style", "style.css"),
"icon": "favicon.png",
"article": "article.html",
"template": "default.html",
}
//go:embed embed/article.html
//go:embed embed/.zona.yml
//go:embed embed/default.html
//go:embed embed/favicon.png
//go:embed embed/footer.md
//go:embed embed/header.md
//go:embed embed/style.css
var embedDir embed.FS
type Settings struct {
Header template.HTML
HeaderName string
Footer template.HTML
FooterName string
StylesheetName string
IconName string
DefaultTemplate string
DefaultTemplateName string
ArticleTemplate string
Stylesheet []byte
StylePath string
Icon []byte
}
var isDefaultStyle bool
// processSetting checks the user's configuration for
// each option. If set, reads the specified file. If not,
// default option is used.
func processSetting(c map[string]interface{}, s string) (string, []byte, error) {
if name, ok := c[s].(string); ok {
val, err := util.ReadFile(name)
if err != nil {
return "", nil, util.ErrorPrepend("Could not read "+s+" specified in config: ", err)
}
return name, val, nil
} else {
val := readEmbed(defaultNames[s])
isDefaultStyle = true
return defaultNames[s], val, nil
}
}
// buildSettings constructs the Settings struct.
func buildSettings(f []byte, outRoot string) (*Settings, error) {
s := &Settings{}
var c map[string]interface{}
// Parse YAML
if err := yaml.Unmarshal(f, &c); err != nil {
return nil, err
}
n, v, err := processSetting(c, "header")
if err != nil {
return nil, err
}
s.HeaderName = n
s.Header = template.HTML(MdToHTML(v))
n, v, err = processSetting(c, "footer")
if err != nil {
return nil, err
}
s.FooterName = n
s.Footer = template.HTML(MdToHTML(v))
isDefaultStyle = false
n, v, err = processSetting(c, "style")
if err != nil {
return nil, err
}
s.StylesheetName = n
s.Stylesheet = v
if isDefaultStyle {
stylePath := filepath.Join(outRoot, defaultNames["stylePath"])
s.StylePath = stylePath
err := util.CreateParents(stylePath)
if err != nil {
return nil, util.ErrorPrepend("Could not create default stylesheet directory: ", err)
}
err = util.WriteFile(s.Stylesheet, stylePath)
if err != nil {
return nil, util.ErrorPrepend("Could not create default stylesheet: ", err)
}
}
n, v, err = processSetting(c, "icon")
if err != nil {
return nil, err
}
s.IconName = n
s.Icon = v
n, v, err = processSetting(c, "template")
if err != nil {
return nil, err
}
s.DefaultTemplateName = n
s.DefaultTemplate = string(v)
artTemp := readEmbed(string(defaultNames["article"]))
s.ArticleTemplate = string(artTemp)
return s, nil
}
// readEmbed reads a file inside the embedded dir
func readEmbed(name string) []byte {
f, err := embedDir.ReadFile("embed/" + name)
if err != nil {
// panic(0)
log.Fatalf("Fatal internal error: Could not read embedded default %s! %u", name, err)
}
return f
}
func GetSettings(root string, outRoot string) *Settings {
var config []byte
configPath := filepath.Join(root, defaultNames["config"])
if !util.FileExists(configPath) {
// Config file does not exist, we used embedded default
config = readEmbed(defaultNames["config"])
} else {
var err error
// config, err = util.ReadFile(filepath.Join(root, configPath))
config, err = util.ReadFile(configPath)
if err != nil {
log.Fatalln("Fatal internal error: Config file exists but could not be read!", err)
}
}
s, err := buildSettings(config, outRoot)
if err != nil {
log.Fatalf("Fatal error: could not parse config: %u\n", err)
}
return s
}

View file

@ -1,166 +0,0 @@
package builder
import (
"bytes"
"fmt"
"io"
"os"
"path/filepath"
"github.com/ficcdaf/zona/internal/util"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/ast"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
)
// This function takes a Markdown document and returns an HTML document.
func MdToHTML(md []byte) []byte {
// create parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
p := parser.NewWithExtensions(extensions)
doc := p.Parse(md)
// build HTML renderer
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
renderer := newZonaRenderer(opts)
return markdown.Render(doc, renderer)
}
// PathIsValid checks if a path is valid.
// If requireFile is set, directories are not considered valid.
func PathIsValid(path string, requireFile bool) bool {
s, err := os.Stat(path)
if os.IsNotExist(err) {
return false
} else if requireFile {
// fmt.Printf("Directory status: %s\n", strconv.FormatBool(s.IsDir()))
return !s.IsDir()
}
return err == nil
}
func processLink(p string) string {
// fmt.Println("Processing link...")
ext := filepath.Ext(p)
// Only process if it points to an existing, local markdown file
if ext == ".md" && filepath.IsLocal(p) {
// fmt.Println("Markdown link detected...")
return util.ChangeExtension(p, ".html")
} else {
return p
}
}
// renderImage outputs an ast Image node as HTML string.
func renderImage(w io.Writer, node *ast.Image, entering bool, next *ast.Text) {
// we add image-container div tag
// here before the opening img tag
if entering {
fmt.Fprintf(w, "<div class=\"image-container\">\n")
fmt.Fprintf(w, `<img src="%s" title="%s"`, node.Destination, node.Title)
if next != nil && len(next.Literal) > 0 {
md := []byte(next.Literal)
html, doc := convertEmbedded(md)
altText := extractPlainText(md, doc)
fmt.Fprintf(w, ` alt="%s">`, altText)
// TODO: render inside a special div?
// is this necessary since this is all inside image-container anyways?
fmt.Fprintf(w, `<small>%s</small>`, html)
} else {
//
io.WriteString(w, ">")
}
} else {
// if it's the closing img tag
// we close the div tag *after*
fmt.Fprintf(w, `</div>`)
fmt.Println("Image node not entering??")
}
}
func renderLink(w io.Writer, l *ast.Link, entering bool) {
if entering {
destPath := processLink(string(l.Destination))
fmt.Fprintf(w, `<a href="%s"`, destPath)
for _, attr := range html.BlockAttrs(l) {
fmt.Fprintf(w, ` %s`, attr)
}
io.WriteString(w, ">")
} else {
io.WriteString(w, "</a>")
}
}
func htmlRenderHookNoImage(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
if link, ok := node.(*ast.Link); ok {
renderLink(w, link, entering)
return ast.GoToNext, true
} else if _, ok := node.(*ast.Image); ok {
// we do not render images
return ast.GoToNext, true
}
return ast.GoToNext, false
}
// htmlRenderHook hooks the HTML renderer and overrides the rendering of certain nodes.
func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
if link, ok := node.(*ast.Link); ok {
renderLink(w, link, entering)
return ast.GoToNext, true
} else if image, ok := node.(*ast.Image); ok {
var nextNode *ast.Text
if entering {
nextNodes := node.GetChildren()
fmt.Println("img next node len:", len(nextNodes))
if len(nextNodes) == 1 {
if textNode, ok := nextNodes[0].(*ast.Text); ok {
nextNode = textNode
}
}
}
renderImage(w, image, entering, nextNode)
// Skip rendering of `nextNode` explicitly
if nextNode != nil {
return ast.SkipChildren, true
}
return ast.GoToNext, true
}
return ast.GoToNext, false
}
// convertEmbedded renders markdown as HTML
// but does NOT render images
// also returns document AST
func convertEmbedded(md []byte) ([]byte, *ast.Node) {
p := parser.NewWithExtensions(parser.CommonExtensions)
doc := p.Parse(md)
htmlFlags := html.CommonFlags | html.HrefTargetBlank
opts := html.RendererOptions{Flags: htmlFlags}
opts.RenderNodeHook = htmlRenderHookNoImage
r := html.NewRenderer(opts)
html := markdown.Render(doc, r)
return html, &doc
}
func newZonaRenderer(opts html.RendererOptions) *html.Renderer {
opts.RenderNodeHook = htmlRenderHook
return html.NewRenderer(opts)
}
// ExtractPlainText walks the AST and extracts plain text from the Markdown input.
func extractPlainText(md []byte, doc *ast.Node) string {
var buffer bytes.Buffer
// Walk the AST and extract text nodes
ast.WalkFunc(*doc, func(node ast.Node, entering bool) ast.WalkStatus {
if textNode, ok := node.(*ast.Text); ok && entering {
buffer.Write(textNode.Literal) // Append the text content
}
return ast.GoToNext
})
return buffer.String()
}

View file

@ -1 +0,0 @@
title: Something

View file

@ -1,28 +0,0 @@
<!doctype html>
<html>
<head>
<title>{{ .Title }}</title>
<link rel="icon" href="{{ .Icon }}" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="UTF-8" />
<link
href="{{ .Stylesheet }}"
rel="stylesheet"
type="text/css"
media="all"
/>
</head>
<body>
<div id="container">
<header id="header">{{ .Header }}</header>
<article id="content">
{{ .Content }}
<nav id="nextprev">
{{ .NextPost }}<br />
{{ .PrevPost }}
</nav>
</article>
<footer id="footer">{{ .Footer }}</footer>
</div>
</body>
</html>

View file

@ -1,22 +0,0 @@
<!doctype html>
<html>
<head>
<title>{{ .Title }}</title>
<link rel="icon" href="{{ .Icon }}" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="UTF-8" />
<link
href="{{ .Stylesheet }}"
rel="stylesheet"
type="text/css"
media="all"
/>
</head>
<body>
<div id="container">
<header id="header">{{ .Header }}<hr></header>
{{ .Content }}
<footer id="footer"><hr>{{ .Footer }}</footer>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1 +0,0 @@
# Footer File

View file

@ -1 +0,0 @@
# Header File

View file

@ -1,148 +0,0 @@
:root {
--main-text-color: white;
--main-bg-color: #1b1b1b;
--main-transparent: rgba(255, 255, 255, 0.15);
--main-small-text-color: rgba(255, 255, 255, 0.45);
}
body {
line-height: 1.6;
font-size: 18px;
font-family: sans-serif;
background: var(--main-bg-color);
color: var(--main-text-color);
padding-left: calc(100vw - 100%);
}
h1 {
margin-block-start: 0.67rem;
margin-block-end: 0.67rem;
font-size: 2rem;
font-weight: bold;
}
article h1:first-of-type {
margin-block-start: 1.67rem;
}
h2 {
margin-block-start: 0.83rem;
margin-block-end: 0.83rem;
font-size: 1.5rem;
font-weight: bold;
}
h3 {
margin-block-start: 1rem;
margin-block-end: 1rem;
font-size: 1.17em;
font-weight: bold;
}
h4 {
margin-block-start: 1.33rem;
margin-block-end: 1.33rem;
font-size: 1rem;
font-weight: bold;
}
article h1 + h4:first-of-type {
margin-block-start: 0rem;
}
h5 {
margin-block-start: 1.67rem;
margin-block-end: 1.67rem;
font-size: 0.83rem;
font-weight: bold;
}
h6 {
margin-block-start: 2.33rem;
margin-block-end: 2.33rem;
font-size: 0.67rem;
font-weight: bold;
}
a {
color: var(--main-text-color);
}
a:hover {
background: var(--main-transparent);
}
img {
width: auto;
height: auto;
}
blockquote {
color: var(--main-small-text-color);
border-left: 3px solid var(--main-transparent);
padding: 0 1rem;
margin-left: 0;
margin-right: 0;
}
hr {
border: none;
height: 1px;
background: var(--main-small-text-color);
}
code {
background: var(--main-transparent);
border-radius: 0.1875rem;
/* padding: .0625rem .1875rem; */
/* margin: 0 .1875rem; */
}
code,
pre {
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
small {
font-size: 0.95rem;
color: var(--main-small-text-color);
}
small a {
color: inherit; /* Inherit the color of the surrounding <small> text */
text-decoration: underline; /* Optional: Keep the underline to indicate a link */
}
.image-container {
text-align: center;
margin: 20px 0; /* Optional: add some spacing around the image container */
}
.image-container img {
/* max-width: 308px; */
max-height: 308px;
}
.image-container small {
display: block; /* Ensure the caption is on a new line */
margin-top: 5px; /* Optional: adjust spacing between image and caption */
}
.image-container small a {
color: inherit; /* Ensure the link color matches the small text */
text-decoration: underline; /* Optional: underline to indicate a link */
}
#header ul {
list-style-type: none;
padding-left: 0;
}
#header li {
display: inline;
font-size: 1.2rem;
margin-right: 1.2rem;
}
#container {
margin: 2.5rem auto;
width: 90%;
max-width: 60ch;
}
#postlistdiv ul {
list-style-type: none;
padding-left: 0;
}
.moreposts {
font-size: 0.95rem;
padding-left: 0.5rem;
}
#nextprev {
text-align: center;
margin-top: 1.4rem;
font-size: 0.95rem;
}
#footer {
color: var(--main-small-text-color);
}

View file

@ -1,86 +0,0 @@
package builder
import (
"bufio"
"bytes"
"errors"
"fmt"
"os"
"gopkg.in/yaml.v3"
)
func processFrontmatter(p string) (*FrontMatter, int, error) {
f, l, err := readFrontmatter(p)
if err != nil {
return nil, l, err
}
var meta FrontMatter
// Parse YAML
if err := yaml.Unmarshal(f, &meta); err != nil {
return nil, l, fmt.Errorf("yaml frontmatter could not be parsed: %w", err)
}
return &meta, l, nil
}
// readFrontmatter reads the file at `path` and scans
// it for --- delimited frontmatter. It does not attempt
// to parse the data, it only scans for the delimiters.
// It returns the frontmatter contents as a byte array
// and its length in lines.
func readFrontmatter(path string) ([]byte, int, error) {
file, err := os.Open(path)
if err != nil {
return nil, 0, err
}
defer file.Close()
lines := make([]string, 0, 10)
s := bufio.NewScanner(file)
i := 0
delims := 0
for s.Scan() {
l := s.Text()
if l == `---` {
if i == 1 && delims == 0 {
// if --- is not the first line, we
// assume the file does not contain frontmatter
// fmt.Println("Delimiter first line")
return nil, 0, nil
}
delims += 1
i += 1
if delims == 2 {
break
}
} else {
if i == 0 {
return nil, 0, nil
}
lines = append(lines, l)
i += 1
}
}
// check whether any errors occurred while scanning
if err := s.Err(); err != nil {
return nil, 0, err
}
if delims == 2 {
l := len(lines)
if l == 0 {
// no valid frontmatter
return nil, 0, errors.New("frontmatter cannot be empty")
}
// convert to byte array
var b bytes.Buffer
for _, line := range lines {
b.WriteString(line + "\n")
}
return b.Bytes(), l, nil
} else {
// not enough delimiters, don't
// treat as frontmatter
s := fmt.Sprintf("%s: frontmatter is missing closing delimiter", path)
return nil, 0, errors.New(s)
}
}

View file

@ -1,183 +0,0 @@
// FILE: internal/builder/build_page_test.go
package builder
import (
"bytes"
"os"
"testing"
)
func TestProcessFrontmatter(t *testing.T) {
// Create a temporary file with valid frontmatter
validContent := `---
title: "Test Title"
description: "Test Description"
---
This is the body of the document.`
tmpfile, err := os.CreateTemp("", "testfile")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write([]byte(validContent)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test the processFrontmatter function with valid frontmatter
meta, l, err := processFrontmatter(tmpfile.Name())
if err != nil {
t.Fatalf("processFrontmatter failed: %v", err)
}
if l != 2 {
t.Errorf("Expected length 2, got %d", l)
}
if meta["title"] != "Test Title" || meta["description"] != "Test Description" {
t.Errorf("Expected title 'Test Title' and description 'Test Description', got title '%s' and description '%s'", meta["title"], meta["description"])
}
// Create a temporary file with invalid frontmatter
invalidContent := `---
title: "Test Title"
description: "Test Description"
There is no closing delimiter???
This is the body of the document.`
tmpfile, err = os.CreateTemp("", "testfile")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write([]byte(invalidContent)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test the processFrontmatter function with invalid frontmatter
_, _, err = processFrontmatter(tmpfile.Name())
if err == nil {
t.Fatalf("Expected error for invalid frontmatter, got nil")
}
// Create a temporary file with invalid frontmatter
invalidContent = `---
---
This is the body of the document.`
tmpfile, err = os.CreateTemp("", "testfile")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write([]byte(invalidContent)); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test the processFrontmatter function with invalid frontmatter
_, _, err = processFrontmatter(tmpfile.Name())
if err == nil {
t.Fatalf("Expected error for invalid frontmatter, got nil")
}
}
func TestReadFrontmatter(t *testing.T) {
tests := []struct {
name string
content string
wantErr bool
wantData []byte
wantLen int
}{
{
name: "Valid frontmatter",
content: `---
title: "Test"
author: "User"
---
Content here`,
wantErr: false,
wantData: []byte("title: \"Test\"\nauthor: \"User\"\n"),
wantLen: 2,
},
{
name: "Missing closing delimiter",
content: `---
title: "Incomplete Frontmatter"`,
wantErr: true,
},
{
name: "Frontmatter later in file",
content: `This is some content
---
title: "Not Frontmatter"
---`,
wantErr: false,
wantData: nil, // Should return nil because `---` is not the first line
wantLen: 0,
},
{
name: "Empty frontmatter",
content: `---
---`,
wantErr: true,
},
{
name: "No frontmatter",
content: `This is just a normal file.`,
wantErr: false,
wantData: nil, // Should return nil as there's no frontmatter
wantLen: 0,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Create a temporary file
tmpFile, err := os.CreateTemp("", "testfile-*.md")
if err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
defer os.Remove(tmpFile.Name())
// Write test content
_, err = tmpFile.WriteString(tc.content)
if err != nil {
t.Fatalf("failed to write to temp file: %v", err)
}
tmpFile.Close()
// Call function under test
data, length, err := readFrontmatter(tmpFile.Name())
// Check for expected error
if tc.wantErr {
if err == nil {
t.Errorf("expected error but got none")
}
} else {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
// Check content
if !bytes.Equal(data, tc.wantData) {
t.Errorf("expected %q, got %q", tc.wantData, data)
}
// Check length
if length != tc.wantLen {
t.Errorf("expected length %d, got %d", tc.wantLen, length)
}
}
})
}
}

View file

@ -1,116 +0,0 @@
package builder
import (
"io/fs"
"path/filepath"
"github.com/ficcdaf/zona/internal/util"
)
type ProcessMemory struct {
// Files holds all page data that may be
// needed while building *other* pages.
Files []*File
// Queue is a FIFO queue of Pages indexes to be built.
// queue should be constructed after all the Pages have been parsed
Queue []int
// Posts is an array of pointers to post pages
// This list is ONLY referenced for generating
// the archive, NOT by the build process!
Posts []*File
}
type File struct {
PageData *PageData
Ext string
InPath string
OutPath string
ShouldCopy bool
HasFrontmatter bool
FrontMatterLen int
}
// NewProcessMemory initializes an empty
// process memory structure
func NewProcessMemory() *ProcessMemory {
f := make([]*File, 0)
q := make([]int, 0)
p := make([]*File, 0)
pm := &ProcessMemory{
f,
q,
p,
}
return pm
}
// processFile processes the metadata only
// of each file
func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings, pm *ProcessMemory) error {
if err != nil {
return err
}
var toProcess bool
var outPath string
var ext string
if entry.IsDir() {
return nil
} else {
ext = filepath.Ext(inPath)
// NOTE: This could be an if statement, but keeping
// the switch makes it easy to extend the logic here later
switch ext {
case ".md":
toProcess = true
outPath = util.ReplaceRoot(inPath, outRoot)
outPath = util.ChangeExtension(outPath, ".html")
outPath = util.Indexify(outPath)
default:
toProcess = false
outPath = util.ReplaceRoot(inPath, outRoot)
}
}
var pd *PageData
hasFrontmatter := false
l := 0
if toProcess {
// process its frontmatter here
m, le, err := processFrontmatter(inPath)
l = le
if err != nil {
return err
}
if m != nil {
hasFrontmatter = true
}
pd = buildPageData(m, inPath, outPath, settings)
} else {
pd = nil
}
file := &File{
pd,
ext,
inPath,
outPath,
!toProcess,
hasFrontmatter,
l,
}
if pd != nil && pd.Type == "post" {
pm.Posts = append(pm.Posts, file)
}
pm.Files = append(pm.Files, file)
return nil
}
func BuildProcessedFiles(pm *ProcessMemory, settings *Settings) error {
for _, f := range pm.Files {
err := BuildFile(f, settings)
if err != nil {
return err
}
}
return nil
}

View file

@ -1,63 +0,0 @@
package builder
import (
"errors"
"io/fs"
"path/filepath"
"github.com/ficcdaf/zona/internal/util"
)
// TODO: Process the metadata and build a queue of files to convert here instead of converting them immediately
func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error {
if err != nil {
return err
}
if !entry.IsDir() {
ext := filepath.Ext(inPath)
outPath := util.ReplaceRoot(inPath, outRoot)
switch ext {
case ".md":
// fmt.Println("Processing markdown...")
outPath = util.ChangeExtension(outPath, ".html")
if err := util.CreateParents(outPath); err != nil {
return err
}
if err := _BuildHtmlFile(inPath, outPath, settings); err != nil {
return errors.Join(errors.New("Error processing file "+inPath), err)
} else {
return nil
}
// If it's not a file we need to process,
// we simply copy it to the destination path.
default:
if err := util.CreateParents(outPath); err != nil {
return err
}
if err := util.CopyFile(inPath, outPath); err != nil {
return errors.Join(errors.New("Error processing file "+inPath), err)
} else {
return nil
}
}
}
// fmt.Printf("Visited: %s\n", inPath)
return nil
}
func Traverse(root string, outRoot string, settings *Settings) error {
walkFunc := func(path string, entry fs.DirEntry, err error) error {
return buildFile(path, entry, err, outRoot, settings)
}
err := filepath.WalkDir(root, walkFunc)
return err
}
func ProcessTraverse(root string, outRoot string, settings *Settings) (*ProcessMemory, error) {
pm := NewProcessMemory()
walkFunc := func(path string, entry fs.DirEntry, err error) error {
return processFile(path, entry, err, outRoot, settings, pm)
}
err := filepath.WalkDir(root, walkFunc)
return pm, err
}

View file

@ -1,111 +0,0 @@
package util
import (
"bufio"
"bytes"
"io"
"os"
)
// WriteFile writes a given byte array to the given path.
func WriteFile(b []byte, p string) error {
f, err := os.Create(p)
if err != nil {
return err
}
_, err = f.Write(b)
defer f.Close()
if err != nil {
return err
}
return nil
}
// ReadFile reads a byte array from a given path.
func ReadFile(p string) ([]byte, error) {
f, err := os.Open(p)
if err != nil {
return nil, err
}
var result []byte
buf := make([]byte, 1024)
for {
n, err := f.Read(buf)
// check for a non EOF error
if err != nil && err != io.EOF {
return nil, err
}
// n==0 when there are no chunks left to read
if n == 0 {
defer f.Close()
break
}
result = append(result, buf[:n]...)
}
return result, nil
}
// CopyFile reads the file at the input path, and write
// it to the output path.
func CopyFile(inPath string, outPath string) error {
inB, err := ReadFile(inPath)
if err != nil {
return err
}
if err := WriteFile(inB, outPath); err != nil {
return err
} else {
return nil
}
}
// ReadNLines reads the first N lines from a file as a single byte array
func ReadNLines(filename string, n int) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var buffer bytes.Buffer
scanner := bufio.NewScanner(file)
for i := 0; i < 3 && scanner.Scan(); i++ {
buffer.Write(scanner.Bytes())
buffer.WriteByte('\n')
}
if err := scanner.Err(); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}
// ReadLineRange reads a file in a given range of lines
func ReadLineRange(filename string, start int, end int) ([]byte, error) {
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
var buffer bytes.Buffer
scanner := bufio.NewScanner(file)
i := 0
for scanner.Scan() {
if i >= start && (i <= end || end == -1) {
buffer.Write(scanner.Bytes())
buffer.WriteByte('\n')
}
if i > end && end != -1 {
break
}
i++
}
if err := scanner.Err(); err != nil {
return nil, err
}
return buffer.Bytes(), nil
}

View file

@ -1,69 +0,0 @@
// FILE: internal/util/file_test.go
package util
import (
"bytes"
"os"
"testing"
)
func TestReadNLines(t *testing.T) {
// Create a temporary file
tmpfile, err := os.CreateTemp("", "testfile")
if err != nil {
t.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
// Write some lines to the temporary file
content := []byte("line1\nline2\nline3\nline4\nline5\n")
if _, err := tmpfile.Write(content); err != nil {
t.Fatal(err)
}
if err := tmpfile.Close(); err != nil {
t.Fatal(err)
}
// Test the ReadNLines function
lines, err := ReadNLines(tmpfile.Name(), 3)
if err != nil {
t.Fatalf("ReadNLines failed: %v", err)
}
expected := []byte("line1\nline2\nline3\n")
if !bytes.Equal(lines, expected) {
t.Errorf("Expected %q, got %q", expected, lines)
}
}
func TestReadLineRange(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
filename string
start int
end int
want []byte
wantErr bool
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotErr := ReadLineRange(tt.filename, tt.start, tt.end)
if gotErr != nil {
if !tt.wantErr {
t.Errorf("ReadLineRange() failed: %v", gotErr)
}
return
}
if tt.wantErr {
t.Fatal("ReadLineRange() succeeded unexpectedly")
}
// TODO: update the condition below to compare got with tt.want.
if true {
t.Errorf("ReadLineRange() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,140 +0,0 @@
// Package util provides general utilities.
package util
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
// CheckExtension checks if the file located at path (string)
// matches the provided extension type
func CheckExtension(path, ext string) error {
if filepath.Ext(path) == ext {
return nil
} else {
return errors.New("Invalid extension.")
}
}
func ChangeExtension(in string, outExt string) string {
return strings.TrimSuffix(in, filepath.Ext(in)) + outExt
}
// find the root. check for a .zona.yml first,
// then check if it's cwd.
func getRoot(path string) string {
marker := ".zona.yml"
for {
parent := filepath.Dir(path)
if parent == "/" {
panic(1)
}
candidate := filepath.Join(parent, marker)
// fmt.Printf("check for: %s\n", candidate)
if FileExists(candidate) {
return parent
} else if parent == "." {
return path
}
path = parent
}
}
func ReplaceRoot(inPath, outRoot string) string {
relPath := strings.TrimPrefix(inPath, getRoot(inPath))
outPath := filepath.Join(outRoot, relPath)
return outPath
}
// Indexify converts format path/file.ext
// into path/file/index.ext
func Indexify(in string) string {
ext := filepath.Ext(in)
trimmed := strings.TrimSuffix(in, ext)
filename := filepath.Base(trimmed)
if filename == "index" {
return in
}
prefix := strings.TrimSuffix(trimmed, filename)
return filepath.Join(prefix, filename, "index"+ext)
}
// InDir checks whether checkPath is
// inside targDir.
func InDir(checkPath string, targDir string) bool {
// fmt.Println("checking dir..")
i := 0
for i < 10 {
parent := filepath.Dir(checkPath)
fmted := filepath.Base(parent)
switch fmted {
case targDir:
// fmt.Printf("%s in %s\n", checkPath, targDir)
return true
case ".":
return false
}
checkPath = parent
i += 1
}
return false
}
// FileExists returns a boolean indicating
// whether something exists at the path.
func FileExists(path string) bool {
_, err := os.Stat(path)
return !os.IsNotExist(err)
}
// CreateParents creates the parent directories required for a given path
func CreateParents(path string) error {
dir := filepath.Dir(path)
// Check if the parent directory already exists
// before trying to create it
if _, dirErr := os.Stat(dir); os.IsNotExist(dirErr) {
// create directories
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err
}
}
return nil
}
func StripTopDir(path string) string {
cleanedPath := filepath.Clean(path)
components := strings.Split(cleanedPath, string(filepath.Separator))
if len(components) <= 1 {
return path
}
return filepath.Join(components[1:]...)
}
func resolveRelativeTo(relPath string, basePath string) string {
baseDir := filepath.Dir(basePath)
combined := filepath.Join(baseDir, relPath)
resolved := filepath.Clean(combined)
return resolved
}
// we want to preserve a valid web-style path
// and convert relative path to web-style
// so we need to see
func NormalizePath(target string, source string) (string, error) {
fmt.Printf("normalizing: %s\n", target)
// empty path is root
if target == "" {
return "/", nil
}
if target[0] == '.' {
resolved := resolveRelativeTo(target, source)
normalized := ReplaceRoot(resolved, "/")
fmt.Printf("Normalized: %s\n", normalized)
return normalized, nil
} else {
return target, nil
}
}

View file

@ -1,35 +0,0 @@
package util_test
import (
"testing"
"github.com/ficcdaf/zona/internal/util"
)
func TestIndexify(t *testing.T) {
tests := []struct {
name string // description of this test case
// Named input parameters for target function.
in string
want string
}{
{
"Simple Path",
"foo/bar/name.html",
"foo/bar/name/index.html",
},
{
"Index Name",
"foo/bar/index.md",
"foo/bar/index.md",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := util.Indexify(tt.in)
if got != tt.want {
t.Errorf("Indexify() = %v, want %v", got, tt.want)
}
})
}
}

View file

@ -1,26 +0,0 @@
package util
// Enqueue appends an int to the queue
func Enqueue(queue []int, element int) []int {
queue = append(queue, element)
return queue
}
// Dequeue pops the first element of the queue
func Dequeue(queue []int) (int, []int) {
element := queue[0] // The first element is the one to be dequeued.
if len(queue) == 1 {
tmp := []int{}
return element, tmp
}
return element, queue[1:] // Slice off the element once it is dequeued.
}
func Tail(queue []int) int {
l := len(queue)
if l == 0 {
return -1
} else {
return l - 1
}
}

View file

@ -1,30 +0,0 @@
package util
import (
"path/filepath"
"strings"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
// PathToWords takes a full path
// and strips separators and extension
// from the file name
func PathToWords(path string) string {
stripped := ChangeExtension(filepath.Base(path), "")
replaced := strings.NewReplacer("-", " ", "_", " ", `\ `, " ").Replace(stripped)
return strings.ToTitle(replaced)
}
func WordsToTitle(words string) string {
caser := cases.Title(language.English)
return caser.String(words)
}
// PathToTitle converts a full path to a string
// in title case
func PathToTitle(path string) string {
words := PathToWords(path)
return WordsToTitle(words)
}

View file

@ -1,23 +0,0 @@
package util
import (
"errors"
"strings"
)
func NormalizeContent(content string) string {
var normalized []string
lines := strings.Split(content, "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if line != "" {
normalized = append(normalized, line)
}
}
return strings.Join(normalized, "\n")
}
// ErrorPrepend returns a new error with a message prepended to the given error.
func ErrorPrepend(m string, err error) error {
return errors.Join(errors.New(m), err)
}

View file

@ -1,14 +0,0 @@
# run go tests
test:
go test ./...
# test outputs
gentest:
#!/bin/bash
if [ -e foobar ]; then
rm -rf foobar
fi
go run cmd/zona/main.go test
# bat foobar/img.html

View file

@ -1,8 +0,0 @@
#!/bin/bash
if [ -e foobar ]; then
rm -rf foobar
fi
go run cmd/zona/main.go test
bat foobar/img.html

View file

@ -1,28 +0,0 @@
<!doctype html>
<html>
<head>
<title>{{ .Title }}</title>
<link rel="icon" href="{{ .Icon }}" type="image/x-icon" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta charset="UTF-8" />
<link
href="{{ .Stylesheet }}"
rel="stylesheet"
type="text/css"
media="all"
/>
</head>
<body>
<div id="container">
<header id="header">{{ .Header }}</header>
<article id="content">
{{ .Content }}
<nav id="nextprev">
{{ .NextPost }}<br />
{{ .PrevPost }}
</nav>
</article>
<footer id="footer">{{ .Footer }}</footer>
</div>
</body>
</html>

View file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

View file

View file

View file

View file

View file

View file

@ -1,3 +0,0 @@
# An image is in this page
![my *alternate* text](assets/pic.png "my title")

View file

@ -1,10 +0,0 @@
---
title: tiltetest
---
# My amazing markdown file!
I can _even_ do **this**!
- Or, I could...
- [Link](page.md) to this file

View file

@ -1,3 +0,0 @@
# My amazing markdown filez 2!
This file gets linked to...

View file

@ -1,11 +0,0 @@
---
title: tiltetest
icon: ../assets/pic.png
---
# My amazing markdown file!
I can _even_ do **this**!
- Or, I could...
- [Link](page.md) to this file

View file

@ -1,148 +0,0 @@
:root {
--main-text-color: white;
--main-bg-color: #1b1b1b;
--main-transparent: rgba(255, 255, 255, 0.15);
--main-small-text-color: rgba(255, 255, 255, 0.45);
}
body {
line-height: 1.6;
font-size: 18px;
font-family: sans-serif;
background: var(--main-bg-color);
color: var(--main-text-color);
padding-left: calc(100vw - 100%);
}
h1 {
margin-block-start: 0.67rem;
margin-block-end: 0.67rem;
font-size: 2rem;
font-weight: bold;
}
article h1:first-of-type {
margin-block-start: 1.67rem;
}
h2 {
margin-block-start: 0.83rem;
margin-block-end: 0.83rem;
font-size: 1.5rem;
font-weight: bold;
}
h3 {
margin-block-start: 1rem;
margin-block-end: 1rem;
font-size: 1.17em;
font-weight: bold;
}
h4 {
margin-block-start: 1.33rem;
margin-block-end: 1.33rem;
font-size: 1rem;
font-weight: bold;
}
article h1 + h4:first-of-type {
margin-block-start: 0rem;
}
h5 {
margin-block-start: 1.67rem;
margin-block-end: 1.67rem;
font-size: 0.83rem;
font-weight: bold;
}
h6 {
margin-block-start: 2.33rem;
margin-block-end: 2.33rem;
font-size: 0.67rem;
font-weight: bold;
}
a {
color: var(--main-text-color);
}
a:hover {
background: var(--main-transparent);
}
img {
width: auto;
height: auto;
}
blockquote {
color: var(--main-small-text-color);
border-left: 3px solid var(--main-transparent);
padding: 0 1rem;
margin-left: 0;
margin-right: 0;
}
hr {
border: none;
height: 1px;
background: var(--main-small-text-color);
}
code {
background: var(--main-transparent);
border-radius: 0.1875rem;
/* padding: .0625rem .1875rem; */
/* margin: 0 .1875rem; */
}
code,
pre {
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
small {
font-size: 0.95rem;
color: var(--main-small-text-color);
}
small a {
color: inherit; /* Inherit the color of the surrounding <small> text */
text-decoration: underline; /* Optional: Keep the underline to indicate a link */
}
.image-container {
text-align: center;
margin: 20px 0; /* Optional: add some spacing around the image container */
}
.image-container img {
/* max-width: 308px; */
max-height: 308px;
}
.image-container small {
display: block; /* Ensure the caption is on a new line */
margin-top: 5px; /* Optional: adjust spacing between image and caption */
}
.image-container small a {
color: inherit; /* Ensure the link color matches the small text */
text-decoration: underline; /* Optional: underline to indicate a link */
}
#header ul {
list-style-type: none;
padding-left: 0;
}
#header li {
display: inline;
font-size: 1.2rem;
margin-right: 1.2rem;
}
#container {
margin: 2.5rem auto;
width: 90%;
max-width: 60ch;
}
#postlistdiv ul {
list-style-type: none;
padding-left: 0;
}
.moreposts {
font-size: 0.95rem;
padding-left: 0.5rem;
}
#nextprev {
text-align: center;
margin-top: 1.4rem;
font-size: 0.95rem;
}
#footer {
color: var(--main-small-text-color);
}

View file

@ -1,3 +0,0 @@
<h1 id="my-amazing-markdown-file">My amazing markdown file!</h1>
<p>I can <em>even</em> do <strong>this</strong>!</p>

View file

@ -1,6 +0,0 @@
# My amazing markdown file!
I can _even_ do **this**!
- Or, I could...
- [Link](page.md) to this file

View file

@ -1,11 +0,0 @@
---
title: Yaml testing file
type: article
---
# My amazing markdown file!
I can _even_ do **this**!
- Or, I could...
- [Link](page.md) to this file

1
zona
View file

@ -1 +0,0 @@
bin/zona