···11+package models
22+33+import (
44+ "encoding/json"
55+ "strings"
66+77+ lexutil "github.com/bluesky-social/indigo/lex/util"
88+)
99+1010+// decodeBlob parses a single JSON-encoded LexBlob as submitted by the markdown
1111+// editor. Returns ok=false for malformed JSON or a blob without a CID.
1212+func decodeBlob(s string) (*lexutil.LexBlob, bool) {
1313+ var b lexutil.LexBlob
1414+ if err := json.Unmarshal([]byte(s), &b); err != nil {
1515+ return nil, false
1616+ }
1717+ if !b.Ref.Defined() {
1818+ return nil, false
1919+ }
2020+ return &b, true
2121+}
2222+2323+// ParseBlobs decodes the blob refs submitted by the editor, keeping only those
2424+// still referenced in body. Referencing them on the record pins them against
2525+// PDS garbage collection; the body filter drops orphans the user removed.
2626+func ParseBlobs(raw []string, body string) []*lexutil.LexBlob {
2727+ return MergeBlobs(nil, raw, body)
2828+}
2929+3030+// MergeBlobs unions already-committed blobs with newly-submitted ones, keeping
3131+// only CIDs still in body. Used on edit to preserve earlier images.
3232+func MergeBlobs(existing []*lexutil.LexBlob, raw []string, body string) []*lexutil.LexBlob {
3333+ seen := make(map[string]struct{})
3434+ var out []*lexutil.LexBlob
3535+3636+ keep := func(b *lexutil.LexBlob) {
3737+ if b == nil || !b.Ref.Defined() {
3838+ return
3939+ }
4040+ cid := b.Ref.String()
4141+ if _, dup := seen[cid]; dup {
4242+ return
4343+ }
4444+ if !strings.Contains(body, cid) {
4545+ return
4646+ }
4747+ seen[cid] = struct{}{}
4848+ out = append(out, b)
4949+ }
5050+5151+ for _, b := range existing {
5252+ keep(b)
5353+ }
5454+ for _, s := range raw {
5555+ if b, ok := decodeBlob(s); ok {
5656+ keep(b)
5757+ }
5858+ }
5959+6060+ return out
6161+}