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, " "), } }