diff --git a/lib/parse/imageref.go b/lib/parse/imageref.go new file mode 100644 index 0000000..509c4c2 --- /dev/null +++ b/lib/parse/imageref.go @@ -0,0 +1,62 @@ +package parse + +import ( + "bytes" + "strings" +) + +// ImageRef represents an image referenced by a filter via OSC 9 marker. +type ImageRef struct { + Index int // Sequential position in the message + Path string // Absolute path to image file on disk + Alt string // Alt text fallback +} + +var oscImagePrefix = []byte("\033]9;image:") + +// ParseImageOSC parses an OSC 9 image marker from a byte slice. +// Format: \033]9;image:path=/tmp/foo.png;alt=text\007 +// The marker may appear anywhere in the line. +// Returns the parsed reference and whether parsing succeeded. +func ParseImageOSC(line []byte) (*ImageRef, bool) { + start := bytes.Index(line, oscImagePrefix) + if start == -1 { + return nil, false + } + rest := line[start+len(oscImagePrefix):] + + // Find string terminator: BEL (\007) or ST (ESC \) + end := -1 + for i := 0; i < len(rest); i++ { + if rest[i] == '\007' { + end = i + break + } + if rest[i] == '\033' && i+1 < len(rest) && rest[i+1] == '\\' { + end = i + break + } + } + if end == -1 { + return nil, false + } + + params := string(rest[:end]) + ref := &ImageRef{} + for _, pair := range strings.Split(params, ";") { + k, v, ok := strings.Cut(pair, "=") + if !ok { + continue + } + switch k { + case "path": + ref.Path = v + case "alt": + ref.Alt = v + } + } + if ref.Path == "" { + return nil, false + } + return ref, true +} diff --git a/lib/parse/imageref_test.go b/lib/parse/imageref_test.go new file mode 100644 index 0000000..223d1d2 --- /dev/null +++ b/lib/parse/imageref_test.go @@ -0,0 +1,95 @@ +package parse + +import ( + "testing" +) + +func TestParseImageOSC_ValidFull(t *testing.T) { + line := []byte("\033]9;image:path=/tmp/aerc-xxx/img0.png;alt=product photo\007") + ref, ok := ParseImageOSC(line) + if !ok { + t.Fatal("expected successful parse") + } + if ref.Path != "/tmp/aerc-xxx/img0.png" { + t.Errorf("path = %q, want /tmp/aerc-xxx/img0.png", ref.Path) + } + if ref.Alt != "product photo" { + t.Errorf("alt = %q, want 'product photo'", ref.Alt) + } +} + +func TestParseImageOSC_PathOnly(t *testing.T) { + line := []byte("\033]9;image:path=/tmp/img.jpg\007") + ref, ok := ParseImageOSC(line) + if !ok { + t.Fatal("expected successful parse") + } + if ref.Path != "/tmp/img.jpg" { + t.Errorf("path = %q, want /tmp/img.jpg", ref.Path) + } + if ref.Alt != "" { + t.Errorf("alt = %q, want empty", ref.Alt) + } +} + +func TestParseImageOSC_EscBackslashTerminator(t *testing.T) { + line := []byte("\033]9;image:path=/tmp/img.png\033\\") + ref, ok := ParseImageOSC(line) + if !ok { + t.Fatal("expected successful parse with ESC\\ terminator") + } + if ref.Path != "/tmp/img.png" { + t.Errorf("path = %q", ref.Path) + } +} + +func TestParseImageOSC_NoPath(t *testing.T) { + line := []byte("\033]9;image:alt=just alt text\007") + _, ok := ParseImageOSC(line) + if ok { + t.Error("expected failure when path is missing") + } +} + +func TestParseImageOSC_NotOSC9(t *testing.T) { + line := []byte("Hello, this is plain text") + _, ok := ParseImageOSC(line) + if ok { + t.Error("expected failure on plain text") + } +} + +func TestParseImageOSC_WrongOSCNumber(t *testing.T) { + line := []byte("\033]8;image:path=/tmp/img.png\007") + _, ok := ParseImageOSC(line) + if ok { + t.Error("expected failure on OSC 8 (not 9)") + } +} + +func TestParseImageOSC_EmptyPath(t *testing.T) { + line := []byte("\033]9;image:path=\007") + _, ok := ParseImageOSC(line) + if ok { + t.Error("expected failure on empty path") + } +} + +func TestParseImageOSC_MarkerEmbeddedInLine(t *testing.T) { + line := []byte("prefix text \033]9;image:path=/tmp/img.png\007 suffix") + ref, ok := ParseImageOSC(line) + if !ok { + t.Fatal("expected successful parse even with surrounding text") + } + if ref.Path != "/tmp/img.png" { + t.Errorf("path = %q", ref.Path) + } +} + +func TestParseImageOSC_NoTerminator(t *testing.T) { + line := []byte("\033]9;image:path=/tmp/img.png") + _, ok := ParseImageOSC(line) + if ok { + t.Error("expected failure when no terminator present") + } +}