package api import ( "strings" // Aliased: this package's own frontmatter type (below) would collide // with the imported package's name otherwise. sharedfm "github.com/mathiasbq/hyperguild/ingestion/internal/frontmatter" ) // frontmatter is an ordered, line-preserving view of a note's YAML // frontmatter block. It deliberately avoids a full YAML round-trip: the // brain writes flat `key: value` frontmatter by hand, and a yaml.v3 // re-marshal would reorder keys and strip comments. Preserving the // original lines verbatim keeps brain_update a surgical edit — only the // keys it manages (updated_at, supersedes, supersede_reason) change. type frontmatter struct { lines []fmLine } // fmLine is one frontmatter line. For `key: value` lines, key and value // are populated; for blank lines, comments, or anything that isn't a // simple scalar pair, key is empty and raw holds the line verbatim. type fmLine struct { key string value string raw string } // parseFrontmatter splits src into its frontmatter block and body. A // frontmatter block is recognised only when the file opens with a `---` // fence and a closing `---` fence follows. Otherwise the whole input is // the body and the returned frontmatter is empty. // // Fence detection itself is frontmatter.Split (hyperguild#138) — this // package's own contribution is purely the ordered, line-preserving fmLine // shape below, which a shared parser has no business owning since it's // specific to how brain_update does a surgical, byte-round-trip edit. func parseFrontmatter(src string) (frontmatter, string) { var fm frontmatter blockBytes, restBytes, ok := sharedfm.Split([]byte(src)) if !ok { return fm, src } // sharedfm.Split's block/rest boundary sits one byte earlier than this // package's always has: block keeps the "\n" immediately before the // closing fence, and rest keeps the "\n" immediately after it (both by // design, for Fix's own reconstruction). This package's block/body // never included either — TrimSuffix/TrimPrefix restore that exact // boundary. Split guarantees ok=true only when "\n---\n" matched, so // both trims are always exactly one byte, never ambiguous. block := strings.TrimSuffix(string(blockBytes), "\n") body := strings.TrimPrefix(string(restBytes), "\n") for _, line := range strings.Split(block, "\n") { // An indented line is a continuation (a YAML block-list item, a // wrapped value) never a top-level key, regardless of whether it // happens to contain a colon — " - 'Chapter 3: The Reckoning'" is // not a key named "- 'Chapter 3" (hyperguild#128). Checked before // Cut so a colon inside one never reaches the key/value path. if line != "" && (line[0] == ' ' || line[0] == '\t') { fm.lines = append(fm.lines, fmLine{raw: line}) continue } key, val, ok := strings.Cut(line, ":") key = strings.TrimSpace(key) if !ok || key == "" || strings.HasPrefix(strings.TrimSpace(line), "#") { fm.lines = append(fm.lines, fmLine{raw: line}) continue } fm.lines = append(fm.lines, fmLine{key: key, value: strings.TrimSpace(val)}) } return fm, body } // get returns the value for key, or "" if absent. func (f *frontmatter) get(key string) string { for _, l := range f.lines { if l.key == key { return l.value } } return "" } // set overrides the value for an existing key in place, or appends a new // `key: value` line when the key is absent. func (f *frontmatter) set(key, value string) { for i := range f.lines { if f.lines[i].key == key { f.lines[i].value = value return } } f.lines = append(f.lines, fmLine{key: key, value: value}) } // del removes every top-level line for key. Continuation lines of a // multi-line value are not tracked per key, so del is only for scalars. func (f *frontmatter) del(key string) { kept := f.lines[:0] for _, l := range f.lines { if l.key != key { kept = append(kept, l) } } f.lines = kept } // render serialises the frontmatter back into a `---`-fenced block. An // empty frontmatter renders to the empty string so bodies without a // header stay header-less. func (f *frontmatter) render() string { if len(f.lines) == 0 { return "" } var b strings.Builder b.WriteString("---\n") for _, l := range f.lines { switch { case l.key == "": b.WriteString(l.raw) case l.value == "": // A key with nothing after the colon (the YAML block-list // shape: "aliases:" followed by indented "- item" lines on // their own raw lines) must stay bare — "key: " with a // trailing space is a different byte sequence than the // original, and this type's whole purpose (see the package // doc) is a byte-preserving round-trip. b.WriteString(l.key) b.WriteByte(':') default: b.WriteString(l.key) b.WriteString(": ") b.WriteString(l.value) } b.WriteByte('\n') } b.WriteString("---\n") return b.String() }