internal/store/anchor.go
1
package store
3
import (
4
"fmt"
5
"strings"
7
yaml "go.yaml.in/yaml/v3"
8
)
10
type Scope string
12
const (
13
ScopeFile Scope = "file"
14
ScopeExcerpt Scope = "excerpt"
15
)
17
func ParseScope(text string) (Scope, error) {
18
switch Scope(text) {
19
case ScopeFile:
20
return ScopeFile, nil
21
case ScopeExcerpt:
22
return ScopeExcerpt, nil
23
}
24
return "", fmt.Errorf("unknown scope %q, want one of %s, %s", text, ScopeFile, ScopeExcerpt)
25
}
27
// Anchor is where an annotation aims. It carries no line, because a line is
28
// something a reader observes rather than something the author decided; that
29
// lives in Status.
30
type Anchor struct {
31
Scope Scope `yaml:"scope"`
32
Excerpt string `yaml:"excerpt,omitempty"`
33
Before string `yaml:"before,omitempty"`
34
After string `yaml:"after,omitempty"`
35
}
37
func (a Anchor) MarshalYAML() (any, error) {
38
node := &yaml.Node{Kind: yaml.MappingNode, Tag: "!!map"}
39
appendScalar := func(key, value string, style yaml.Style) {
40
node.Content = append(node.Content,
41
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: key},
42
&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: value, Style: style},
43
)
44
}
45
appendScalar("scope", string(a.Scope), 0)
46
if a.Excerpt != "" {
47
appendScalar("excerpt", a.Excerpt, safeStringStyle(a.Excerpt))
48
}
49
if a.Before != "" {
50
appendScalar("before", a.Before, safeStringStyle(a.Before))
51
}
52
if a.After != "" {
53
appendScalar("after", a.After, safeStringStyle(a.After))
54
}
55
return node, nil
56
}
58
func safeStringStyle(value string) yaml.Style {
59
if strings.Contains(value, "\t") {
60
return yaml.DoubleQuotedStyle
61
}
62
if strings.Contains(value, "\n") {
63
return yaml.LiteralStyle
64
}
65
return 0
66
}
68
func (a Anchor) Validate(id string) error {
69
switch a.Scope {
70
case ScopeFile:
71
if a.Excerpt != "" || a.Before != "" || a.After != "" {
72
return fmt.Errorf("annotation %s: file anchor must not carry excerpt context", id)
73
}
74
return nil
75
case ScopeExcerpt:
76
if a.Excerpt == "" {
77
return fmt.Errorf("annotation %s: excerpt anchor requires a non-empty excerpt", id)
78
}
79
if err := validateContext("before", a.Before); err != nil {
80
return fmt.Errorf("annotation %s: %w", id, err)
81
}
82
if err := validateContext("after", a.After); err != nil {
83
return fmt.Errorf("annotation %s: %w", id, err)
84
}
85
return nil
86
default:
87
_, err := ParseScope(string(a.Scope))
88
return fmt.Errorf("annotation %s: %w", id, err)
89
}
90
}
92
func validateContext(name, context string) error {
93
if context == "" {
94
return nil
95
}
96
if strings.Count(strings.TrimSuffix(context, "\n"), "\n") >= 3 {
97
return fmt.Errorf("anchor.%s contains more than three lines", name)
98
}
99
return nil
100
}