1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113 |
// 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
}
|