// Package doc turns a content file into what Patchwork compares between // versions: normalized text, frontmatter, the author's editorial fields, the // diffable blocks, and the hashes that decide whether anything changed. package doc import ( "crypto/sha256" "encoding/hex" "encoding/json" ) // Options name the frontmatter keys Patchwork reads. type Options struct { NoteKey string KindKey string MaturityKey string // DiffKeys are frontmatter keys whose changes count as content changes // (title, summary, tags). Every other key is ignored. DiffKeys []string } // DefaultOptions match the field names in the moonflower demo. func DefaultOptions() Options { return Options{NoteKey: "revision", KindKey: "revision_kind", MaturityKey: "maturity", DiffKeys: []string{"title", "summary", "tags"}} } // Doc is one parsed version of a file. type Doc struct { Text string Frontmatter map[string]any FrontmatterErr error Body string Blocks []Block Note string Kind string Maturity string Title string // RawHash changes when anything in the file changes beyond line endings, // BOM, Unicode form and trailing spaces. RawHash string // ContentHash covers what readers see: the body without ignored regions // plus the DiffKeys values. Editing only the note leaves it unchanged. ContentHash string } // Parse reads one version of a file. func Parse(raw []byte, opt Options) *Doc { d := &Doc{Text: Normalize(raw)} d.RawHash = hashText(d.Text) yamlSrc, body, ok := splitFrontmatter(d.Text) d.Body = body d.Frontmatter = map[string]any{} if ok { d.Frontmatter, d.FrontmatterErr = parseFrontmatter(yamlSrc) } d.Note = stringField(d.Frontmatter, opt.NoteKey) d.Kind = stringField(d.Frontmatter, opt.KindKey) d.Maturity = stringField(d.Frontmatter, opt.MaturityKey) visible := stripIgnored(body) d.Blocks = SplitBlocks(visible) d.Title = stringField(d.Frontmatter, "title") if d.Title == "" { for _, b := range d.Blocks { if b.Kind == KindHeading { d.Title = b.Text break } } } meta := map[string]any{} for _, k := range opt.DiffKeys { if v, ok := d.Frontmatter[k]; ok { meta[k] = v } } metaJSON, _ := json.Marshal(meta) // map keys marshal sorted sum := sha256.Sum256([]byte(hashText(visible) + "\x00" + string(metaJSON))) d.ContentHash = hex.EncodeToString(sum[:]) return d } // MetaValue returns a DiffKeys value in a stable, comparable form. func (d *Doc) MetaValue(key string) any { v, ok := d.Frontmatter[key] if !ok { return nil } return v } // Change kinds an author may declare. var Kinds = []string{"correction", "expansion", "position", "reorganization"} // Maturities an author may declare, from least to most grown. var Maturities = []string{"seed", "sprout", "bloom"} // Valid reports whether v is empty or one of allowed. func Valid(v string, allowed []string) bool { if v == "" { return true } for _, a := range allowed { if v == a { return true } } return false }