summaryrefslogtreecommitdiff
path: root/internal/draft/parser.go
diff options
from:
to:
context:
space:
mode:
authorGravatar Saya Andy <saya.andy@posteo.com> 2026-08-01 19:58:57 +0700
committerGravatar Saya Andy <saya.andy@posteo.com> 2026-08-01 19:58:57 +0700
commit684d0b6a57d2eb79730ade63baccdc6e59bebc29 (patch)
tree956c89c81965b8c974c8497e3719d0b16d332512 /internal/draft/parser.go
parent129e48683f6fc0615b606a737f2afcd50538bb2e (diff)
downloadarticlator-main.tar.gz
articlator-main.zip
feat: repurpose metadata parser as article transcoder for saya.uz andmain
telegram
Diffstat (limited to 'internal/draft/parser.go')
-rw-r--r--internal/draft/parser.go179
1 files changed, 179 insertions, 0 deletions
diff --git a/internal/draft/parser.go b/internal/draft/parser.go
new file mode 100644
index 0000000..2761c87
--- /dev/null
+++ b/internal/draft/parser.go
@@ -0,0 +1,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, " "),
+ }
+}