summaryrefslogtreecommitdiff
path: root/internal/draft/parser.go
blob: 2761c8771c055585b360d7f92170cf3de37fa369 (plain)
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, " "),
	}
}