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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
package draft
import (
"regexp"
"strings"
"github.com/SayaAndy/saya-today-article-metadata-add/internal/frontmatter"
)
// layoutTokenRe matches gallery layout hints like "2x", "3x" that are
// meaningful only to the sayauz renderer and must be dropped elsewhere.
var layoutTokenRe = regexp.MustCompile(`^\d+x$`)
type BlockKind int
const (
TextBlock BlockKind = iota
GalleryBlock
)
// Block is one ordered piece of a section: either a run of markdown text or a
// gallery. Exactly one of Text / Gallery is meaningful depending on Kind.
type Block struct {
Kind BlockKind
Text string
Gallery *Gallery
}
type Gallery struct {
Timezone string
KeyPrefix string
Photos []Photo
}
type Photo struct {
Key string // KeyPrefix + filename, e.g. "Sevsk/20241008-093833.jpg"
Caption string
}
// Section is the content between two "..." separator lines, split into ordered
// text and gallery blocks.
type Section struct {
Index int
Blocks []Block
}
type Document struct {
SourcePath string
Codename string
Metadata *frontmatter.Metadata
RawContent []byte
Body string
Sections []Section
}
// ParseDraft parses a draft file: frontmatter metadata, the markdown body, and
// the body split into sections (on "..." lines) of ordered text/gallery blocks.
func ParseDraft(sourcePath, codename string, content []byte) (*Document, error) {
metadata, body, err := frontmatter.ParseFrontmatter(content)
if err != nil {
return nil, err
}
doc := &Document{
SourcePath: sourcePath,
Codename: codename,
Metadata: metadata,
RawContent: content,
Body: string(body),
}
doc.Sections = splitSections(doc.Body)
return doc, nil
}
func splitSections(body string) []Section {
lines := strings.Split(body, "\n")
var sections []Section
var chunk []string
flush := func() {
blocks := parseBlocks(chunk)
chunk = nil
if len(blocks) == 0 {
return
}
sections = append(sections, Section{Index: len(sections), Blocks: blocks})
}
for _, line := range lines {
if strings.TrimSpace(line) == "..." {
flush()
continue
}
chunk = append(chunk, line)
}
flush()
return sections
}
func parseBlocks(lines []string) []Block {
var blocks []Block
var text []string
flushText := func() {
joined := strings.TrimSpace(strings.Join(text, "\n"))
text = nil
if joined != "" {
blocks = append(blocks, Block{Kind: TextBlock, Text: joined})
}
}
for i := 0; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if strings.HasPrefix(trimmed, "{Gallery:") {
flushText()
gallery, next := parseGallery(lines, i)
if gallery != nil {
blocks = append(blocks, Block{Kind: GalleryBlock, Gallery: gallery})
}
i = next
continue
}
text = append(text, lines[i])
}
flushText()
return blocks
}
// parseGallery reads a {Gallery:...} block starting at lines[start] and returns
// the parsed gallery plus the index of the closing {/Gallery} line (or the last
// consumed line if unterminated).
func parseGallery(lines []string, start int) (*Gallery, int) {
header := strings.TrimSpace(lines[start])
header = strings.TrimPrefix(header, "{Gallery:")
header = strings.TrimSuffix(header, "}")
tz, keyPrefix, _ := strings.Cut(header, ":")
gallery := &Gallery{Timezone: tz, KeyPrefix: keyPrefix}
i := start + 1
for ; i < len(lines); i++ {
trimmed := strings.TrimSpace(lines[i])
if trimmed == "{/Gallery}" {
break
}
if trimmed == "" {
continue
}
gallery.Photos = append(gallery.Photos, parsePhoto(keyPrefix, trimmed))
}
return gallery, i
}
func parsePhoto(keyPrefix, line string) Photo {
parts := strings.Split(line, "|")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
filename := parts[0]
var captionParts []string
for _, p := range parts[1:] {
if p == "" || layoutTokenRe.MatchString(p) {
continue
}
captionParts = append(captionParts, p)
}
return Photo{
Key: keyPrefix + filename,
Caption: strings.Join(captionParts, " "),
}
}
|