Compare commits
30 commits
dev-stable
...
dev
Author | SHA1 | Date | |
---|---|---|---|
9a94052b42 | |||
3355bc5544 | |||
99bc128578 | |||
8895ba969c | |||
c0429fa92b | |||
577eeeab2d | |||
af2f071a5a | |||
bdd9e63fdf | |||
59ead0f26a | |||
c263879904 | |||
4b62ed116e | |||
fdec4b6f25 | |||
988d4ba42e | |||
fdb8753538 | |||
116fb6a883 | |||
65b62ef9a6 | |||
0ecad9e96a | |||
4629200510 | |||
7644a31016 | |||
4315348cf5 | |||
4d27581f0a | |||
587085df86 | |||
af81617db5 | |||
35c14f09c0 | |||
709a2738f9 | |||
ffe5ea4efc | |||
525cbcd980 | |||
ce706e4ff9 | |||
0c1c842bcd | |||
fb67ef046a |
26 changed files with 949 additions and 156 deletions
17
.build.yml
Normal file
17
.build.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
image: alpine/latest
|
||||
packages:
|
||||
- git
|
||||
- openssh
|
||||
secrets:
|
||||
- 0639564d-6995-4e2e-844b-2f8feb0b7fb1
|
||||
environment:
|
||||
repo: zona
|
||||
github: git@github.com:ficcdaf/zona.git
|
||||
tasks:
|
||||
- mirror: |
|
||||
ssh-keyscan github.com >> ~/.ssh/known_hosts
|
||||
cd "$repo"
|
||||
git remote add github "$github"
|
||||
git push --mirror github
|
||||
|
||||
|
52
TODO.md
Normal file
52
TODO.md
Normal file
|
@ -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!
|
|
@ -42,8 +42,18 @@ func main() {
|
|||
|
||||
}
|
||||
settings := builder.GetSettings(*rootPath, "foobar")
|
||||
err := builder.Traverse(*rootPath, "foobar", settings)
|
||||
// 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)
|
||||
}
|
||||
|
|
7
go.mod
7
go.mod
|
@ -1,11 +1,10 @@
|
|||
module github.com/ficcdaf/zona
|
||||
|
||||
// go 1.23.2
|
||||
go 1.23.4
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require golang.org/x/text v0.20.0
|
||||
require golang.org/x/text v0.23.0
|
||||
|
|
|
@ -2,6 +2,7 @@ package builder
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
|
@ -24,11 +25,21 @@ type PageData struct {
|
|||
FooterName string
|
||||
Footer template.HTML
|
||||
Template string
|
||||
Type string
|
||||
}
|
||||
|
||||
type Metadata map[string]interface{}
|
||||
type Metadata map[string]any
|
||||
|
||||
func processWithYaml(f []byte) (Metadata, []byte, error) {
|
||||
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")
|
||||
|
@ -39,31 +50,36 @@ func processWithYaml(f []byte) (Metadata, []byte, error) {
|
|||
// Separate YAML from rest of document
|
||||
split := strings.SplitN(normalized, "---\n", 3)
|
||||
if len(split) < 3 {
|
||||
return nil, nil, fmt.Errorf("Invalid frontmatter format.")
|
||||
return nil, nil, fmt.Errorf("invalid frontmatter format")
|
||||
}
|
||||
var meta Metadata
|
||||
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
|
||||
return &meta, []byte(split[2]), nil
|
||||
}
|
||||
|
||||
func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData {
|
||||
func buildPageData(m *FrontMatter, in string, out string, settings *Settings) *PageData {
|
||||
p := &PageData{}
|
||||
if title, ok := m["title"].(string); ok {
|
||||
p.Title = util.WordsToTitle(title)
|
||||
if m != nil && m.Title != "" {
|
||||
p.Title = util.WordsToTitle(m.Title)
|
||||
} else {
|
||||
p.Title = util.PathToTitle(in)
|
||||
}
|
||||
if icon, ok := m["icon"].(string); ok {
|
||||
p.Icon = icon
|
||||
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 style, ok := m["style"].(string); ok {
|
||||
stylePath = style
|
||||
if m != nil && m.Style != "" {
|
||||
stylePath = m.Style
|
||||
} else {
|
||||
stylePath = settings.StylePath
|
||||
}
|
||||
|
@ -74,30 +90,34 @@ func buildPageData(m Metadata, in string, out string, settings *Settings) *PageD
|
|||
log.Fatalln("Error calculating stylesheet path: ", err)
|
||||
}
|
||||
p.Stylesheet = relPath
|
||||
if header, ok := m["header"].(string); ok {
|
||||
p.HeaderName = header
|
||||
|
||||
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 footer, ok := m["footer"].(string); ok {
|
||||
p.FooterName = footer
|
||||
if m != nil && m.Footer != "" {
|
||||
p.FooterName = m.Footer
|
||||
p.Footer = settings.Footer
|
||||
} else {
|
||||
p.FooterName = settings.FooterName
|
||||
p.Footer = settings.Footer
|
||||
}
|
||||
if t, ok := m["type"].(string); ok && t == "article" || t == "post" {
|
||||
// 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 ConvertFile(in string, out string, settings *Settings) error {
|
||||
func _BuildHtmlFile(in string, out string, settings *Settings) error {
|
||||
mdPre, err := util.ReadFile(in)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -126,3 +146,58 @@ func ConvertFile(in string, out string, settings *Settings) error {
|
|||
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
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
var defaultNames = map[string]string{
|
||||
"config": "config.yml",
|
||||
"config": ".zona.yml",
|
||||
"header": "header.md",
|
||||
"footer": "footer.md",
|
||||
"style": "style.css",
|
||||
|
@ -22,7 +22,7 @@ var defaultNames = map[string]string{
|
|||
}
|
||||
|
||||
//go:embed embed/article.html
|
||||
//go:embed embed/config.yml
|
||||
//go:embed embed/.zona.yml
|
||||
//go:embed embed/default.html
|
||||
//go:embed embed/favicon.png
|
||||
//go:embed embed/footer.md
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package builder
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
@ -53,6 +54,33 @@ func processLink(p string) string {
|
|||
}
|
||||
}
|
||||
|
||||
// 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))
|
||||
|
@ -66,15 +94,73 @@ func renderLink(w io.Writer, l *ast.Link, entering bool) {
|
|||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
package builder_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ficcdaf/zona/internal/builder"
|
||||
"github.com/ficcdaf/zona/internal/util"
|
||||
)
|
||||
|
||||
func TestMdToHTML(t *testing.T) {
|
||||
md := []byte("# Hello World\n\nThis is a test.")
|
||||
expectedHTML := "<h1 id=\"hello-world\">Hello World</h1>\n<p>This is a test.</p>\n"
|
||||
nExpectedHTML := util.NormalizeContent(expectedHTML)
|
||||
html, err := builder.MdToHTML(md)
|
||||
nHtml := util.NormalizeContent(string(html))
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if nHtml != nExpectedHTML {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", expectedHTML, html)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "test.txt")
|
||||
content := []byte("Hello, World!")
|
||||
|
||||
err := builder.WriteFile(content, path)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Verify file content
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading file: %v", err)
|
||||
}
|
||||
if string(data) != string(content) {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", content, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadFile(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "test.txt")
|
||||
content := []byte("Hello, World!")
|
||||
|
||||
err := os.WriteFile(path, content, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing file: %v", err)
|
||||
}
|
||||
|
||||
data, err := builder.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
if string(data) != string(content) {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", content, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
src := filepath.Join(t.TempDir(), "source.txt")
|
||||
dst := filepath.Join(t.TempDir(), "dest.txt")
|
||||
content := []byte("File content for testing.")
|
||||
|
||||
err := os.WriteFile(src, content, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing source file: %v", err)
|
||||
}
|
||||
|
||||
err = builder.CopyFile(src, dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Verify destination file content
|
||||
data, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading destination file: %v", err)
|
||||
}
|
||||
if string(data) != string(content) {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", content, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConvertFile(t *testing.T) {
|
||||
src := filepath.Join(t.TempDir(), "test.md")
|
||||
dst := filepath.Join(t.TempDir(), "test.html")
|
||||
mdContent := []byte("# Test Title\n\nThis is Markdown content.")
|
||||
nExpectedHTML := util.NormalizeContent("<h1 id=\"test-title\">Test Title</h1>\n<p>This is Markdown content.</p>\n")
|
||||
|
||||
err := os.WriteFile(src, mdContent, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("Error writing source Markdown file: %v", err)
|
||||
}
|
||||
|
||||
err = builder.ConvertFile(src, dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Expected no error, got %v", err)
|
||||
}
|
||||
|
||||
// Verify destination HTML content
|
||||
data, err := os.ReadFile(dst)
|
||||
if err != nil {
|
||||
t.Fatalf("Error reading HTML file: %v", err)
|
||||
}
|
||||
if util.NormalizeContent(string(data)) != nExpectedHTML {
|
||||
t.Errorf("Expected:\n%s\nGot:\n%s", nExpectedHTML, data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeExtension(t *testing.T) {
|
||||
input := "test.md"
|
||||
output := builder.ChangeExtension(input, ".html")
|
||||
expected := "test.html"
|
||||
|
||||
if output != expected {
|
||||
t.Errorf("Expected %s, got %s", expected, output)
|
||||
}
|
||||
}
|
86
internal/builder/frontmatter.go
Normal file
86
internal/builder/frontmatter.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
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)
|
||||
}
|
||||
}
|
183
internal/builder/frontmatter_test.go
Normal file
183
internal/builder/frontmatter_test.go
Normal file
|
@ -0,0 +1,183 @@
|
|||
// 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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
116
internal/builder/process.go
Normal file
116
internal/builder/process.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
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
|
||||
}
|
|
@ -8,7 +8,8 @@ import (
|
|||
"github.com/ficcdaf/zona/internal/util"
|
||||
)
|
||||
|
||||
func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error {
|
||||
// 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
|
||||
}
|
||||
|
@ -22,7 +23,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se
|
|||
if err := util.CreateParents(outPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ConvertFile(inPath, outPath, settings); err != nil {
|
||||
if err := _BuildHtmlFile(inPath, outPath, settings); err != nil {
|
||||
return errors.Join(errors.New("Error processing file "+inPath), err)
|
||||
} else {
|
||||
return nil
|
||||
|
@ -46,8 +47,17 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se
|
|||
|
||||
func Traverse(root string, outRoot string, settings *Settings) error {
|
||||
walkFunc := func(path string, entry fs.DirEntry, err error) error {
|
||||
return processFile(path, entry, err, outRoot, settings)
|
||||
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
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
@ -56,3 +58,54 @@ func CopyFile(inPath string, outPath string) error {
|
|||
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
|
||||
}
|
||||
|
|
69
internal/util/file_test.go
Normal file
69
internal/util/file_test.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package util
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
@ -22,16 +23,24 @@ 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 == "." {
|
||||
break
|
||||
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
|
||||
}
|
||||
// fmt.Println("getRoot: ", path)
|
||||
return path
|
||||
}
|
||||
|
||||
func ReplaceRoot(inPath, outRoot string) string {
|
||||
|
@ -40,6 +49,40 @@ func ReplaceRoot(inPath, outRoot string) string {
|
|||
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 {
|
||||
|
@ -69,3 +112,29 @@ func StripTopDir(path string) string {
|
|||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
35
internal/util/path_test.go
Normal file
35
internal/util/path_test.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
26
internal/util/queue.go
Normal file
26
internal/util/queue.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
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
|
||||
}
|
||||
}
|
14
justfile
Normal file
14
justfile
Normal file
|
@ -0,0 +1,14 @@
|
|||
# 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
|
|
@ -5,4 +5,4 @@ fi
|
|||
|
||||
go run cmd/zona/main.go test
|
||||
|
||||
bat foobar/in.html
|
||||
bat foobar/img.html
|
||||
|
|
0
test/.zona.yml
Normal file
0
test/.zona.yml
Normal file
BIN
test/assets/pic.png
Normal file
BIN
test/assets/pic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 KiB |
3
test/img.md
Normal file
3
test/img.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# An image is in this page
|
||||
|
||||

|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
type: article
|
||||
title: tiltetest
|
||||
---
|
||||
|
||||
# My amazing markdown file!
|
||||
|
|
11
test/posts/in.md
Normal file
11
test/posts/in.md
Normal file
|
@ -0,0 +1,11 @@
|
|||
---
|
||||
title: tiltetest
|
||||
icon: ../assets/pic.png
|
||||
---
|
||||
|
||||
# My amazing markdown file!
|
||||
|
||||
I can _even_ do **this**!
|
||||
|
||||
- Or, I could...
|
||||
- [Link](page.md) to this file
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
title: Yaml testing file
|
||||
type: article
|
||||
---
|
||||
|
||||
# My amazing markdown file!
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue