Compare commits
1 commit
dev
...
dev-stable
Author | SHA1 | Date | |
---|---|---|---|
9766fb01e3 |
25 changed files with 156 additions and 932 deletions
52
TODO.md
52
TODO.md
|
@ -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!
|
|
|
@ -42,18 +42,8 @@ func main() {
|
||||||
|
|
||||||
}
|
}
|
||||||
settings := builder.GetSettings(*rootPath, "foobar")
|
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 {
|
if err != nil {
|
||||||
fmt.Printf("Error: %s\n", err.Error())
|
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,10 +1,11 @@
|
||||||
module github.com/ficcdaf/zona
|
module github.com/ficcdaf/zona
|
||||||
|
|
||||||
go 1.24.2
|
// go 1.23.2
|
||||||
|
go 1.23.4
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
github.com/gomarkdown/markdown v0.0.0-20241105142532-d03b89096d81
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/text v0.23.0
|
require golang.org/x/text v0.20.0
|
||||||
|
|
|
@ -2,7 +2,6 @@ package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"log"
|
"log"
|
||||||
|
@ -25,21 +24,11 @@ type PageData struct {
|
||||||
FooterName string
|
FooterName string
|
||||||
Footer template.HTML
|
Footer template.HTML
|
||||||
Template string
|
Template string
|
||||||
Type string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Metadata map[string]any
|
type Metadata map[string]interface{}
|
||||||
|
|
||||||
type FrontMatter struct {
|
func processWithYaml(f []byte) (Metadata, []byte, error) {
|
||||||
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
|
// Check if the file has valid metadata
|
||||||
trimmed := bytes.TrimSpace(f)
|
trimmed := bytes.TrimSpace(f)
|
||||||
normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n")
|
normalized := strings.ReplaceAll(string(trimmed), "\r\n", "\n")
|
||||||
|
@ -50,36 +39,31 @@ func processWithYaml(f []byte) (*FrontMatter, []byte, error) {
|
||||||
// Separate YAML from rest of document
|
// Separate YAML from rest of document
|
||||||
split := strings.SplitN(normalized, "---\n", 3)
|
split := strings.SplitN(normalized, "---\n", 3)
|
||||||
if len(split) < 3 {
|
if len(split) < 3 {
|
||||||
return nil, nil, fmt.Errorf("invalid frontmatter format")
|
return nil, nil, fmt.Errorf("Invalid frontmatter format.")
|
||||||
}
|
}
|
||||||
var meta FrontMatter
|
var meta Metadata
|
||||||
// Parse YAML
|
// Parse YAML
|
||||||
if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil {
|
if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return &meta, []byte(split[2]), nil
|
return meta, []byte(split[2]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildPageData(m *FrontMatter, in string, out string, settings *Settings) *PageData {
|
func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData {
|
||||||
p := &PageData{}
|
p := &PageData{}
|
||||||
if m != nil && m.Title != "" {
|
if title, ok := m["title"].(string); ok {
|
||||||
p.Title = util.WordsToTitle(m.Title)
|
p.Title = util.WordsToTitle(title)
|
||||||
} else {
|
} else {
|
||||||
p.Title = util.PathToTitle(in)
|
p.Title = util.PathToTitle(in)
|
||||||
}
|
}
|
||||||
if m != nil && m.Icon != "" {
|
if icon, ok := m["icon"].(string); ok {
|
||||||
i, err := util.NormalizePath(m.Icon, in)
|
p.Icon = icon
|
||||||
if err != nil {
|
|
||||||
p.Icon = settings.IconName
|
|
||||||
} else {
|
|
||||||
p.Icon = i
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
p.Icon = settings.IconName
|
p.Icon = settings.IconName
|
||||||
}
|
}
|
||||||
var stylePath string
|
var stylePath string
|
||||||
if m != nil && m.Style != "" {
|
if style, ok := m["style"].(string); ok {
|
||||||
stylePath = m.Style
|
stylePath = style
|
||||||
} else {
|
} else {
|
||||||
stylePath = settings.StylePath
|
stylePath = settings.StylePath
|
||||||
}
|
}
|
||||||
|
@ -90,34 +74,30 @@ func buildPageData(m *FrontMatter, in string, out string, settings *Settings) *P
|
||||||
log.Fatalln("Error calculating stylesheet path: ", err)
|
log.Fatalln("Error calculating stylesheet path: ", err)
|
||||||
}
|
}
|
||||||
p.Stylesheet = relPath
|
p.Stylesheet = relPath
|
||||||
|
if header, ok := m["header"].(string); ok {
|
||||||
if m != nil && m.Header != "" {
|
p.HeaderName = header
|
||||||
p.HeaderName = m.Header
|
|
||||||
// for now we use default anyways
|
// for now we use default anyways
|
||||||
p.Header = settings.Header
|
p.Header = settings.Header
|
||||||
} else {
|
} else {
|
||||||
p.HeaderName = settings.HeaderName
|
p.HeaderName = settings.HeaderName
|
||||||
p.Header = settings.Header
|
p.Header = settings.Header
|
||||||
}
|
}
|
||||||
if m != nil && m.Footer != "" {
|
if footer, ok := m["footer"].(string); ok {
|
||||||
p.FooterName = m.Footer
|
p.FooterName = footer
|
||||||
p.Footer = settings.Footer
|
p.Footer = settings.Footer
|
||||||
} else {
|
} else {
|
||||||
p.FooterName = settings.FooterName
|
p.FooterName = settings.FooterName
|
||||||
p.Footer = settings.Footer
|
p.Footer = settings.Footer
|
||||||
}
|
}
|
||||||
// TODO: Don't hard code posts dir name
|
if t, ok := m["type"].(string); ok && t == "article" || t == "post" {
|
||||||
if (m != nil && (m.Type == "article" || m.Type == "post")) || util.InDir(in, "posts") {
|
|
||||||
p.Template = (settings.ArticleTemplate)
|
p.Template = (settings.ArticleTemplate)
|
||||||
p.Type = "post"
|
|
||||||
} else {
|
} else {
|
||||||
p.Template = (settings.DefaultTemplate)
|
p.Template = (settings.DefaultTemplate)
|
||||||
p.Type = ""
|
|
||||||
}
|
}
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
func _BuildHtmlFile(in string, out string, settings *Settings) error {
|
func ConvertFile(in string, out string, settings *Settings) error {
|
||||||
mdPre, err := util.ReadFile(in)
|
mdPre, err := util.ReadFile(in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -146,58 +126,3 @@ func _BuildHtmlFile(in string, out string, settings *Settings) error {
|
||||||
err = util.WriteFile(output.Bytes(), out)
|
err = util.WriteFile(output.Bytes(), out)
|
||||||
return err
|
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{
|
var defaultNames = map[string]string{
|
||||||
"config": ".zona.yml",
|
"config": "config.yml",
|
||||||
"header": "header.md",
|
"header": "header.md",
|
||||||
"footer": "footer.md",
|
"footer": "footer.md",
|
||||||
"style": "style.css",
|
"style": "style.css",
|
||||||
|
@ -22,7 +22,7 @@ var defaultNames = map[string]string{
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed embed/article.html
|
//go:embed embed/article.html
|
||||||
//go:embed embed/.zona.yml
|
//go:embed embed/config.yml
|
||||||
//go:embed embed/default.html
|
//go:embed embed/default.html
|
||||||
//go:embed embed/favicon.png
|
//go:embed embed/favicon.png
|
||||||
//go:embed embed/footer.md
|
//go:embed embed/footer.md
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
package builder
|
package builder
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
@ -54,33 +53,6 @@ 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) {
|
func renderLink(w io.Writer, l *ast.Link, entering bool) {
|
||||||
if entering {
|
if entering {
|
||||||
destPath := processLink(string(l.Destination))
|
destPath := processLink(string(l.Destination))
|
||||||
|
@ -94,73 +66,15 @@ 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) {
|
func htmlRenderHook(w io.Writer, node ast.Node, entering bool) (ast.WalkStatus, bool) {
|
||||||
if link, ok := node.(*ast.Link); ok {
|
if link, ok := node.(*ast.Link); ok {
|
||||||
renderLink(w, link, entering)
|
renderLink(w, link, entering)
|
||||||
return ast.GoToNext, true
|
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
|
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 {
|
func newZonaRenderer(opts html.RendererOptions) *html.Renderer {
|
||||||
opts.RenderNodeHook = htmlRenderHook
|
opts.RenderNodeHook = htmlRenderHook
|
||||||
return html.NewRenderer(opts)
|
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()
|
|
||||||
}
|
|
||||||
|
|
122
internal/builder/convert_test.go
Normal file
122
internal/builder/convert_test.go
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -8,8 +8,7 @@ import (
|
||||||
"github.com/ficcdaf/zona/internal/util"
|
"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 processFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error {
|
||||||
func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, settings *Settings) error {
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -23,7 +22,7 @@ func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, sett
|
||||||
if err := util.CreateParents(outPath); err != nil {
|
if err := util.CreateParents(outPath); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := _BuildHtmlFile(inPath, outPath, settings); err != nil {
|
if err := ConvertFile(inPath, outPath, settings); err != nil {
|
||||||
return errors.Join(errors.New("Error processing file "+inPath), err)
|
return errors.Join(errors.New("Error processing file "+inPath), err)
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -47,17 +46,8 @@ func buildFile(inPath string, entry fs.DirEntry, err error, outRoot string, sett
|
||||||
|
|
||||||
func Traverse(root string, outRoot string, settings *Settings) error {
|
func Traverse(root string, outRoot string, settings *Settings) error {
|
||||||
walkFunc := func(path string, entry fs.DirEntry, err error) error {
|
walkFunc := func(path string, entry fs.DirEntry, err error) error {
|
||||||
return buildFile(path, entry, err, outRoot, settings)
|
return processFile(path, entry, err, outRoot, settings)
|
||||||
}
|
}
|
||||||
err := filepath.WalkDir(root, walkFunc)
|
err := filepath.WalkDir(root, walkFunc)
|
||||||
return err
|
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,8 +1,6 @@
|
||||||
package util
|
package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
@ -58,54 +56,3 @@ func CopyFile(inPath string, outPath string) error {
|
||||||
return nil
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,6 @@ package util
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -23,24 +22,16 @@ func ChangeExtension(in string, outExt string) string {
|
||||||
return strings.TrimSuffix(in, filepath.Ext(in)) + outExt
|
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 {
|
func getRoot(path string) string {
|
||||||
marker := ".zona.yml"
|
|
||||||
for {
|
for {
|
||||||
parent := filepath.Dir(path)
|
parent := filepath.Dir(path)
|
||||||
if parent == "/" {
|
if parent == "." {
|
||||||
panic(1)
|
break
|
||||||
}
|
|
||||||
candidate := filepath.Join(parent, marker)
|
|
||||||
// fmt.Printf("check for: %s\n", candidate)
|
|
||||||
if FileExists(candidate) {
|
|
||||||
return parent
|
|
||||||
} else if parent == "." {
|
|
||||||
return path
|
|
||||||
}
|
}
|
||||||
path = parent
|
path = parent
|
||||||
}
|
}
|
||||||
|
// fmt.Println("getRoot: ", path)
|
||||||
|
return path
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReplaceRoot(inPath, outRoot string) string {
|
func ReplaceRoot(inPath, outRoot string) string {
|
||||||
|
@ -49,40 +40,6 @@ func ReplaceRoot(inPath, outRoot string) string {
|
||||||
return outPath
|
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
|
// FileExists returns a boolean indicating
|
||||||
// whether something exists at the path.
|
// whether something exists at the path.
|
||||||
func FileExists(path string) bool {
|
func FileExists(path string) bool {
|
||||||
|
@ -112,29 +69,3 @@ func StripTopDir(path string) string {
|
||||||
}
|
}
|
||||||
return filepath.Join(components[1:]...)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
14
justfile
14
justfile
|
@ -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
|
|
|
@ -5,4 +5,4 @@ fi
|
||||||
|
|
||||||
go run cmd/zona/main.go test
|
go run cmd/zona/main.go test
|
||||||
|
|
||||||
bat foobar/img.html
|
bat foobar/in.html
|
||||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 1 KiB |
|
@ -1,3 +0,0 @@
|
||||||
# An image is in this page
|
|
||||||
|
|
||||||

|
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
title: tiltetest
|
type: article
|
||||||
---
|
---
|
||||||
|
|
||||||
# My amazing markdown file!
|
# My amazing markdown 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
|
|
|
@ -1,6 +1,5 @@
|
||||||
---
|
---
|
||||||
title: Yaml testing file
|
title: Yaml testing file
|
||||||
type: article
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# My amazing markdown file!
|
# My amazing markdown file!
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue