diff --git a/internal/builder/build_page.go b/internal/builder/build_page.go index 96441da..dfa0241 100644 --- a/internal/builder/build_page.go +++ b/internal/builder/build_page.go @@ -50,32 +50,6 @@ func processWithYaml(f []byte) (Metadata, []byte, error) { return meta, []byte(split[2]), nil } -func processFrontmatter(p string) (Metadata, error) { - // TODO: Also save index of the line where the actual document starts? - f, err := util.ReadFile(p) - if err != nil { - return nil, err - } - // 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 frontmatter, return nil -- handled by caller - return nil, nil - } - // Separate YAML from rest of document - split := strings.SplitN(normalized, "---\n", 3) - if len(split) < 3 { - return nil, fmt.Errorf("invalid frontmatter format") - } - var meta Metadata - // Parse YAML - if err := yaml.Unmarshal([]byte(split[1]), &meta); err != nil { - return nil, err - } - return meta, nil -} - func buildPageData(m Metadata, in string, out string, settings *Settings) *PageData { p := &PageData{} if title, ok := m["title"].(string); ok { diff --git a/internal/builder/frontmatter.go b/internal/builder/frontmatter.go new file mode 100644 index 0000000..73dfa89 --- /dev/null +++ b/internal/builder/frontmatter.go @@ -0,0 +1,81 @@ +package builder + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "os" + + "gopkg.in/yaml.v3" +) + +func processFrontmatter(p string) (Metadata, int, error) { + f, l, err := readFrontmatter(p) + if err != nil { + return nil, l, err + } + var meta Metadata + // 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 > 0 && delims == 0 { + // if --- is not the first line, we + // assume the file does not contain frontmatter + return nil, 0, nil + } + delims += 1 + i += 1 + if delims == 2 { + break + } + } else { + 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 + return nil, 0, errors.New("frontmatter is missing closing delimiter") + } +} diff --git a/internal/builder/build_page_test.go b/internal/builder/frontmatter_test.go similarity index 66% rename from internal/builder/build_page_test.go rename to internal/builder/frontmatter_test.go index cf0a311..8661984 100644 --- a/internal/builder/build_page_test.go +++ b/internal/builder/frontmatter_test.go @@ -28,10 +28,13 @@ This is the body of the document.` } // Test the processFrontmatter function with valid frontmatter - meta, err := processFrontmatter(tmpfile.Name()) + 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"]) @@ -41,6 +44,7 @@ This is the body of the document.` invalidContent := `--- title: "Test Title" description: "Test Description" +There is no closing delimiter??? This is the body of the document.` tmpfile, err = os.CreateTemp("", "testfile") @@ -57,7 +61,30 @@ This is the body of the document.` } // Test the processFrontmatter function with invalid frontmatter - _, err = processFrontmatter(tmpfile.Name()) + _, _, 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") } diff --git a/internal/builder/process.go b/internal/builder/process.go index 260be0a..45de14a 100644 --- a/internal/builder/process.go +++ b/internal/builder/process.go @@ -70,7 +70,7 @@ func processFile(inPath string, entry fs.DirEntry, err error, outRoot string, se hasFrontmatter := false if toProcess { // process its frontmatter here - m, err := processFrontmatter(inPath) + m, _, err := processFrontmatter(inPath) if err != nil { return err }