diff --git a/.build.yml b/.build.yml
index 15d2cd9..7546403 100644
--- a/.build.yml
+++ b/.build.yml
@@ -14,3 +14,4 @@ tasks:
git remote add github "$github"
git push --mirror github
+
diff --git a/README.md b/README.md
index 5de03c3..066093e 100644
--- a/README.md
+++ b/README.md
@@ -1,65 +1,49 @@
# Zona
-**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.**
+Zona is a tool for building a static website, optimized for lightweight blogs following minimalist design principles.
-[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)
+**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
-> 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.
-
-
+- [Design Goals](#design-goals)
+- [v1 Features](#v1-features)
+- [Roadmap](#roadmap)
+- [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.
+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.
-## Features Implemented
+## v1 Features
- Write pages purely in Markdown.
- Single-command build process.
- Lightweight output.
- Sensible default template and stylesheet.
-- Configuration file.
-- Internal links preserved.
-- Custom image element parsing & formatting.
+- Configuration entirely optional, but very powerful.
- Site header and footer defined in Markdown.
-- YAML frontmatter support.
-
-## Planned Features
-
-- Automatically treat contents of `posts/` directory as blog posts.
-- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery`
- elements.
+- Declarative metadata per Markdown file.
+- Automatically generated `Archive`, `Recent Posts`, and `Image Gallery` elements.
- Support for custom stylesheets, favicons, and page templates.
-- Image optimization and dithering.
-- Custom markdown tags that expand to user-defined templates.
-- Live preview server.
-- Robust tests.
+
+## 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.
## 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.
diff --git a/README.md.orig b/README.md.orig
new file mode 100644
index 0000000..9665995
--- /dev/null
+++ b/README.md.orig
@@ -0,0 +1,106 @@
+# 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)
diff --git a/TODO.md b/TODO.md
new file mode 100644
index 0000000..005efcf
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,52 @@
+# 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!
diff --git a/cmd/zona/main.go b/cmd/zona/main.go
new file mode 100644
index 0000000..e1d8a59
--- /dev/null
+++ b/cmd/zona/main.go
@@ -0,0 +1,59 @@
+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)
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..8f10aa7
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,10 @@
+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
diff --git a/install.sh b/install.sh
new file mode 100755
index 0000000..c63bb5d
--- /dev/null
+++ b/install.sh
@@ -0,0 +1,5 @@
+#!/bin/bash
+
+go mod tidy
+go build -o bin/zona ./cmd/zona
+sudo cp -f bin/zona /usr/bin/zona
diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go
new file mode 100644
index 0000000..4fa9b3b
--- /dev/null
+++ b/internal/builder/build_page.go
@@ -0,0 +1,203 @@
+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
+}
diff --git a/internal/builder/config.go b/internal/builder/config.go
new file mode 100644
index 0000000..5d960e5
--- /dev/null
+++ b/internal/builder/config.go
@@ -0,0 +1,155 @@
+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
+}
diff --git a/internal/builder/convert.go b/internal/builder/convert.go
new file mode 100644
index 0000000..12831b3
--- /dev/null
+++ b/internal/builder/convert.go
@@ -0,0 +1,166 @@
+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, "
\n")
+ fmt.Fprintf(w, ` 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, `%s`, html)
+ } else {
+ //
+ io.WriteString(w, ">")
+ }
+ } else {
+ // if it's the closing img tag
+ // we close the div tag *after*
+ fmt.Fprintf(w, `
`)
+ 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, `")
+ } else {
+ io.WriteString(w, "")
+ }
+}
+
+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()
+}
diff --git a/internal/builder/embed/.zona.yml b/internal/builder/embed/.zona.yml
new file mode 100644
index 0000000..cd304c3
--- /dev/null
+++ b/internal/builder/embed/.zona.yml
@@ -0,0 +1 @@
+title: Something
diff --git a/internal/builder/embed/article.html b/internal/builder/embed/article.html
new file mode 100644
index 0000000..eb677d8
--- /dev/null
+++ b/internal/builder/embed/article.html
@@ -0,0 +1,28 @@
+
+
+
+ {{ .Title }}
+
+
+
+
+
+
+