From 684d0b6a57d2eb79730ade63baccdc6e59bebc29 Mon Sep 17 00:00:00 2001 From: Saya Andy Date: Sat, 1 Aug 2026 19:58:57 +0700 Subject: feat: repurpose metadata parser as article transcoder for saya.uz and telegram --- internal/draft/parser.go | 179 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 internal/draft/parser.go (limited to 'internal/draft/parser.go') 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, " "), + } +} -- cgit v1.3.1+13