Initial commit
Test / test (push) Successful in 7m5s
Release / gates (push) Successful in 7m28s
Release / build (amd64, freebsd) (push) Successful in 2m52s
Release / build (amd64, linux) (push) Successful in 2m46s
Release / build (arm64, freebsd) (push) Successful in 2m22s
Release / build (arm64, linux) (push) Successful in 2m38s
Release / build (loong64, linux) (push) Successful in 2m7s
Release / build (riscv64, linux) (push) Successful in 2m17s
Release / release (push) Successful in 1m0s

Assisted-by: GLM 5.3
This commit is contained in:
2026-09-29 10:03:32 +02:00
commit f8ed33df83
206 changed files with 44165 additions and 0 deletions
+426
View File
@@ -0,0 +1,426 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: PolyForm-Noncommercial-1.0.0
package frontmatter
import (
"fmt"
"strings"
"testing"
"time"
"sourcedock.dev/petrbalvin/interpres/v2"
"sourcedock.dev/petrbalvin/volumen/internal/tomlfile"
)
const sample = `+++
title = "Ahoj"
slug = "ahoj"
tags = ["go", "blog"]
draft = false
date = 2026-08-18
[translations]
en = "hello"
+++
# Nadpis
Tělo článku.
`
func TestParse(t *testing.T) {
meta, body, _ := Parse(sample)
if got, _ := meta.Get("title"); got != "Ahoj" {
t.Fatalf("title = %v", got)
}
if got, _ := meta.Get("draft"); got != false {
t.Fatalf("draft = %v", got)
}
if !strings.HasPrefix(body, "# Nadpis") {
t.Fatalf("body = %q", body)
}
trans, ok := meta.Get("translations")
if !ok {
t.Fatal("translations missing")
}
tr := trans.(map[string]any)
if tr["en"] != "hello" {
t.Fatalf("translations = %v", tr)
}
}
func TestParseKeyOrderPreserved(t *testing.T) {
meta, _, _ := Parse(sample)
want := []string{"title", "slug", "tags", "draft", "date", "translations"}
got := meta.Keys()
if len(got) != len(want) {
t.Fatalf("keys = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Fatalf("keys = %v, want %v", got, want)
}
}
}
func TestParseWithoutFrontmatter(t *testing.T) {
meta, body, _ := Parse("just text\n")
if meta.Len() != 0 {
t.Fatalf("metadata = %v, want empty", meta.Map())
}
if body != "just text\n" {
t.Fatalf("body = %q", body)
}
}
func TestParseUnclosedFrontmatter(t *testing.T) {
meta, body, _ := Parse("+++\ntitle = \"x\"\nno closing\n")
if meta.Len() != 0 {
t.Fatalf("metadata = %v, want empty", meta.Map())
}
if body != "+++\ntitle = \"x\"\nno closing\n" {
t.Fatalf("body = %q", body)
}
}
func TestParseInvalidToml(t *testing.T) {
meta, body, err := Parse("+++\nnot valid = = =\n+++\nbody\n")
if err == nil {
t.Fatalf("want error for invalid TOML, got metadata=%v body=%q", meta.Map(), body)
}
}
func TestParseCRLF(t *testing.T) {
crlf := strings.ReplaceAll(sample, "\n", "\r\n")
meta, body, _ := Parse(crlf)
if got, _ := meta.Get("title"); got != "Ahoj" {
t.Fatalf("title = %v", got)
}
if !strings.HasPrefix(body, "# Nadpis") {
t.Fatalf("body = %q", body)
}
}
func TestParseMetadataBodyOffset(t *testing.T) {
_, bodyStart, _ := ParseMetadata("+++\ntitle = \"x\"\n+++\n\ntext")
if bodyStart < 0 {
t.Fatal("bodyStart < 0")
}
rest := ("+++\ntitle = \"x\"\n+++\n\ntext")[bodyStart:]
if rest != "text" {
t.Fatalf("rest = %q, want %q", rest, "text")
}
}
func TestDumpRoundTrip(t *testing.T) {
meta, body, _ := Parse(sample)
out, err := Dump(meta, body)
if err != nil {
t.Fatalf("Dump: %v", err)
}
meta2, body2, _ := Parse(out)
if body2 != body {
t.Fatalf("body changed:\n%q\nvs\n%q", body2, body)
}
for _, key := range []string{"title", "slug", "draft"} {
a, _ := meta.Get(key)
b, _ := meta2.Get(key)
if a != b {
t.Fatalf("key %q: %v -> %v", key, a, b)
}
}
tags1, _ := meta.Get("tags")
tags2, _ := meta2.Get("tags")
if len(tags1.([]any)) != len(tags2.([]any)) {
t.Fatalf("tags changed: %v -> %v", tags1, tags2)
}
tr1 := meta.Map()["translations"].(map[string]any)
tr2 := meta2.Map()["translations"].(map[string]any)
if tr1["en"] != tr2["en"] {
t.Fatalf("translations changed: %v -> %v", tr1, tr2)
}
// Round-trip must be stable: dumping again yields identical bytes.
out2, err := Dump(meta2, body2)
if err != nil {
t.Fatalf("Dump 2: %v", err)
}
if out != out2 {
t.Fatalf("round-trip not stable:\n%s\nvs\n%s", out, out2)
}
}
func TestDumpEmptyMetadata(t *testing.T) {
out, err := Dump(NewMeta(), "body")
if err != nil {
t.Fatalf("Dump: %v", err)
}
if out != "+++\n+++\n\nbody\n" {
t.Fatalf("out = %q", out)
}
}
func TestDumpAddsTrailingNewline(t *testing.T) {
out, err := Dump(NewMeta(), "body without newline")
if err != nil {
t.Fatalf("Dump: %v", err)
}
if !strings.HasSuffix(out, "body without newline\n") {
t.Fatalf("out = %q", out)
}
}
func TestDumpValueTypes(t *testing.T) {
meta := NewMeta()
meta.Set("s", "řetězec s \"")
meta.Set("n", int64(42))
meta.Set("f", 3.5)
meta.Set("fint", 3.0)
meta.Set("b", true)
meta.Set("d", interpres.LocalDate{Time: time.Date(2026, 8, 18, 0, 0, 0, 0, time.UTC)})
meta.Set("arr", []any{"a", "b"})
out, err := Dump(meta, "x")
if err != nil {
t.Fatalf("Dump: %v", err)
}
for _, want := range []string{
`s = "řetězec s \""`,
"n = 42",
"f = 3.5",
"fint = 3.0",
"b = true",
"d = 2026-08-18",
`arr = ["a", "b"]`,
} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in:\n%s", want, out)
}
}
}
func TestDumpRejectsNil(t *testing.T) {
meta := NewMeta()
meta.Set("bad", nil)
if _, err := Dump(meta, "x"); err == nil {
t.Fatal("want error for nil value")
}
}
func TestDumpQuotedKeys(t *testing.T) {
meta := NewMeta()
meta.Set("with space", "v")
out, err := Dump(meta, "x")
if err != nil {
t.Fatalf("Dump: %v", err)
}
if !strings.Contains(out, `"with space" = "v"`) {
t.Fatalf("out = %q", out)
}
}
func TestMetaOperations(t *testing.T) {
meta := NewMeta()
meta.Set("a", int64(1))
meta.Set("b", int64(2))
meta.Set("a", int64(3))
if meta.Len() != 2 {
t.Fatalf("Len = %d", meta.Len())
}
if got, _ := meta.Get("a"); got != int64(3) {
t.Fatalf("a = %v", got)
}
if _, ok := meta.Get("b"); !ok {
t.Fatal("Has(b) = false")
}
meta.Delete("a")
meta.Delete("missing")
if keys := meta.Keys(); len(keys) != 1 || keys[0] != "b" {
t.Fatalf("keys = %v", keys)
}
}
func TestMetaFromMapUnknownOrderKeysSorted(t *testing.T) {
m := map[string]any{"b": int64(2), "a": int64(1), "c": int64(3)}
meta := MetaFromMap(m, []string{"c"})
keys := meta.Keys()
if keys[0] != "c" || keys[1] != "a" || keys[2] != "b" {
t.Fatalf("keys = %v", keys)
}
}
func TestParseClosingDelimiterWithoutTrailingNewline(t *testing.T) {
// A file ending exactly on the closing delimiter must not panic.
meta, body, err := Parse("+++\nslug = \"x\"\n+++")
if err != nil {
t.Fatalf("Parse: %v", err)
}
if got, _ := meta.Get("slug"); got != "x" {
t.Fatalf("slug = %v", got)
}
if body != "" {
t.Fatalf("body = %q, want empty", body)
}
// The metadata-only variant behaves the same.
if _, offset, err := ParseMetadata("+++\n+++"); err != nil || offset > len("+++\n+++") {
t.Fatalf("offset = %d, err = %v", offset, err)
}
}
// A byte-order mark must not demote the frontmatter to body: draft and
// publish_at live there, and a file saved as UTF-8 with BOM keeps its
// meaning.
func TestParseStripsByteOrderMark(t *testing.T) {
meta, body, err := Parse("\ufeff+++\ntitle = \"BOM\"\nslug = \"bom\"\ndraft = true\n+++\n\nbody text\n")
if err != nil {
t.Fatalf("Parse: %v", err)
}
if title, _ := meta.Get("title"); title != "BOM" {
t.Fatalf("title = %v, want BOM", title)
}
if !strings.Contains(body, "body text") || strings.Contains(body, "+++") {
t.Fatalf("body = %q", body)
}
if v, present := meta.Get("draft"); !present || v != true {
t.Fatal("draft flag lost behind the BOM")
}
}
// An array of tables in the frontmatter survives a save: it parses, so
// it must also serialise, or the post can never be edited again.
func TestDumpArraysOfTables(t *testing.T) {
src := "+++\ntitle = \"T\"\nslug = \"t\"\ninline = [{a = \"b\", n = 3}]\n\n[[chapters]]\nx = 1\n\n[[chapters]]\nx = 2\n+++\n\nbody\n"
meta, _, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
out, err := Dump(meta, "body\n")
if err != nil {
t.Fatalf("Dump rejected an array of tables: %v", err)
}
// The array of tables keeps its header form instead of being
// flattened into inline tables.
if !strings.Contains(out, "[[chapters]]") {
t.Fatalf("chapters header form lost:\n%s", out)
}
meta2, body2, err := Parse(out)
if err != nil {
t.Fatalf("round-trip parse: %v\n%s", err, out)
}
if body2 != "body\n" {
t.Fatalf("body = %q", body2)
}
chapters, _ := meta2.Get("chapters")
if got := len(tomlfile.Tables(chapters)); got != 2 {
t.Fatalf("chapters hold %d tables, want 2:\n%s", got, out)
}
inline, _ := meta2.Get("inline")
if got := len(tomlfile.Tables(inline)); got != 1 {
t.Fatalf("inline holds %d tables, want 1:\n%s", got, out)
}
}
// Comments in the frontmatter survive a save: they sit above the key
// the author explained, and a save has no business deleting them.
func TestDumpKeepsComments(t *testing.T) {
src := "+++\n# the visible name\ntitle = \"T\"\nslug = \"c\"\n\n# where it also lives\n[translations]\nen = \"hello\"\n+++\n\nbody\n"
meta, body, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
out, err := Dump(meta, body)
if err != nil {
t.Fatalf("Dump: %v", err)
}
for _, want := range []string{"# the visible name", "# where it also lives"} {
if !strings.Contains(out, want) {
t.Fatalf("missing %q in:\n%s", want, out)
}
}
// Round-trip must be stable: dumping again yields identical bytes.
meta2, _, err := Parse(out)
if err != nil {
t.Fatalf("round-trip parse: %v\n%s", err, out)
}
out2, err := Dump(meta2, body)
if err != nil {
t.Fatalf("Dump 2: %v", err)
}
if out != out2 {
t.Fatalf("round-trip not stable:\n%s\nvs\n%s", out, out2)
}
}
// A nested table keeps the order its keys were written in, not the
// sorted order a map would give.
func TestDumpKeepsNestedKeyOrder(t *testing.T) {
src := "+++\ntitle = \"T\"\nslug = \"o\"\n[translations]\nzz = \"last\"\naa = \"first\"\n+++\n\nbody\n"
meta, body, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
out, err := Dump(meta, body)
if err != nil {
t.Fatalf("Dump: %v", err)
}
zz := strings.Index(out, "zz = ")
aa := strings.Index(out, "aa = ")
if zz < 0 || aa < 0 || zz > aa {
t.Fatalf("nested order re-sorted:\n%s", out)
}
}
// A clone is a deep copy: it carries the comments with it, and mutating
// it leaves the original untouched.
func TestCloneKeepsCommentsAndIsolates(t *testing.T) {
src := "+++\ntitle = \"T\"\nslug = \"c\"\n\n# the visible name\ntitle_note = \"x\"\n[translations]\nen = \"hello\"\n+++\n\nbody\n"
meta, _, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
clone := meta.Clone()
clone.Set("title", "Changed")
tr, _ := clone.Get("translations")
tr.(map[string]any)["en"] = "mutated"
orig, _ := meta.Get("title")
if orig != "T" {
t.Fatalf("original title = %v, want T", orig)
}
origTr, _ := meta.Get("translations")
if origTr.(map[string]any)["en"] != "hello" {
t.Fatalf("original translations mutated: %v", origTr)
}
out, err := Dump(clone, "body\n")
if err != nil {
t.Fatalf("Dump: %v", err)
}
if !strings.Contains(out, "# the visible name") || !strings.Contains(out, `title = "Changed"`) {
t.Fatalf("clone lost a comment or the new value:\n%s", out)
}
}
// A line reading "+++" inside a multi-line string is content, not the
// closing delimiter; the frontmatter ends at the real one.
func TestParseDelimiterInsideMultilineString(t *testing.T) {
src := "+++\ntitle = \"T\"\nslug = \"ml\"\nbody = \"\"\"\n+++\nnot the end\n\"\"\"\n+++\nreal body\n"
meta, body, err := Parse(src)
if err != nil {
t.Fatalf("Parse: %v", err)
}
value, _ := meta.Get("body")
if got := fmt.Sprintf("%v", value); !strings.Contains(got, "not the end") {
t.Fatalf("multiline value = %q", got)
}
if !strings.HasPrefix(body, "real body") {
t.Fatalf("body = %q", body)
}
literal := "+++\ntitle = \"T\"\nslug = \"ml\"\nnote = '''\n+++\nliteral content\n'''\n+++\nreal body\n"
_, body2, err := Parse(literal)
if err != nil {
t.Fatalf("Parse literal: %v", err)
}
if !strings.HasPrefix(body2, "real body") {
t.Fatalf("literal body = %q", body2)
}
}