code.pablomurad.com

patchwork

Publishes the revisions of your pages as a feed, with notes and readable diffs, written in Go

internal/doc/doc.go raw
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
}