diff --git a/lib/parse/extract_images.go b/lib/parse/extract_images.go new file mode 100644 index 0000000..9db1774 --- /dev/null +++ b/lib/parse/extract_images.go @@ -0,0 +1,41 @@ +package parse + +import ( + "bufio" + "bytes" + "fmt" + "io" + + "git.sr.ht/~rjarry/aerc/lib/log" +) + +// ExtractImages scans a reader for OSC 9 image markers, replaces them with +// null-byte-delimited placeholders (\x00IMG:N\x00), and returns the cleaned +// reader plus a slice of ImageRef structs. +// +// Lines containing valid image markers are replaced entirely with the +// placeholder. Lines with invalid markers (e.g. missing path) pass through +// unchanged. +func ExtractImages(r io.Reader) (io.Reader, []ImageRef) { + var images []ImageRef + buf := bytes.NewBuffer(nil) + scanner := bufio.NewScanner(r) + scanner.Buffer(nil, 1024*1024*1024) + for scanner.Scan() { + line := scanner.Bytes() + if bytes.Contains(line, oscImagePrefix) { + if ref, ok := ParseImageOSC(line); ok { + ref.Index = len(images) + images = append(images, *ref) + fmt.Fprintf(buf, "\x00IMG:%d\x00\n", ref.Index) + continue + } + } + buf.Write(line) + buf.WriteByte('\n') + } + if err := scanner.Err(); err != nil { + log.Warnf("ExtractImages: scan error: %v", err) + } + return buf, images +} diff --git a/lib/parse/extract_images_test.go b/lib/parse/extract_images_test.go new file mode 100644 index 0000000..afec89b --- /dev/null +++ b/lib/parse/extract_images_test.go @@ -0,0 +1,101 @@ +package parse + +import ( + "io" + "strings" + "testing" +) + +func TestExtractImages_NoMarkers(t *testing.T) { + input := "Hello\nWorld\nNo images here\n" + r, images := ExtractImages(strings.NewReader(input)) + if len(images) != 0 { + t.Errorf("got %d images, want 0", len(images)) + } + out, _ := io.ReadAll(r) + if string(out) != input { + t.Errorf("output = %q, want %q", string(out), input) + } +} + +func TestExtractImages_SingleMarker(t *testing.T) { + input := "Line 1\n\033]9;image:path=/tmp/img.png;alt=photo\007\nLine 2\n" + r, images := ExtractImages(strings.NewReader(input)) + if len(images) != 1 { + t.Fatalf("got %d images, want 1", len(images)) + } + if images[0].Path != "/tmp/img.png" { + t.Errorf("path = %q", images[0].Path) + } + if images[0].Alt != "photo" { + t.Errorf("alt = %q", images[0].Alt) + } + if images[0].Index != 0 { + t.Errorf("index = %d, want 0", images[0].Index) + } + out, _ := io.ReadAll(r) + expected := "Line 1\n\x00IMG:0\x00\nLine 2\n" + if string(out) != expected { + t.Errorf("output = %q, want %q", string(out), expected) + } +} + +func TestExtractImages_MultipleMarkers(t *testing.T) { + input := "Text\n\033]9;image:path=/tmp/a.png\007\nMiddle\n\033]9;image:path=/tmp/b.jpg;alt=second\007\nEnd\n" + r, images := ExtractImages(strings.NewReader(input)) + if len(images) != 2 { + t.Fatalf("got %d images, want 2", len(images)) + } + if images[0].Index != 0 || images[1].Index != 1 { + t.Errorf("indices: %d, %d", images[0].Index, images[1].Index) + } + if images[1].Path != "/tmp/b.jpg" { + t.Errorf("second path = %q", images[1].Path) + } + out, _ := io.ReadAll(r) + if !strings.Contains(string(out), "\x00IMG:0\x00") { + t.Error("missing placeholder 0") + } + if !strings.Contains(string(out), "\x00IMG:1\x00") { + t.Error("missing placeholder 1") + } +} + +func TestExtractImages_EmptyInput(t *testing.T) { + r, images := ExtractImages(strings.NewReader("")) + if len(images) != 0 { + t.Errorf("got %d images, want 0", len(images)) + } + out, _ := io.ReadAll(r) + if len(out) != 0 { + t.Errorf("expected empty output, got %q", string(out)) + } +} + +func TestExtractImages_MarkerAtStartAndEnd(t *testing.T) { + input := "\033]9;image:path=/tmp/first.png\007\nContent\n\033]9;image:path=/tmp/last.png\007\n" + r, images := ExtractImages(strings.NewReader(input)) + if len(images) != 2 { + t.Fatalf("got %d images, want 2", len(images)) + } + out, _ := io.ReadAll(r) + lines := strings.Split(strings.TrimSuffix(string(out), "\n"), "\n") + if lines[0] != "\x00IMG:0\x00" { + t.Errorf("first line = %q, want placeholder", lines[0]) + } + if lines[2] != "\x00IMG:1\x00" { + t.Errorf("last line = %q, want placeholder", lines[2]) + } +} + +func TestExtractImages_InvalidMarkerPassesThrough(t *testing.T) { + input := "Text\n\033]9;image:alt=no path\007\nMore text\n" + r, images := ExtractImages(strings.NewReader(input)) + if len(images) != 0 { + t.Errorf("got %d images, want 0 (invalid marker)", len(images)) + } + out, _ := io.ReadAll(r) + if !strings.Contains(string(out), "Text") { + t.Error("text content lost") + } +} diff --git a/lib/parse/imageref.go b/lib/parse/imageref.go index 509c4c2..7fea321 100644 --- a/lib/parse/imageref.go +++ b/lib/parse/imageref.go @@ -7,7 +7,7 @@ import ( // ImageRef represents an image referenced by a filter via OSC 9 marker. type ImageRef struct { - Index int // Sequential position in the message + Index int // Sequential position in the message; set by ExtractImages, not ParseImageOSC Path string // Absolute path to image file on disk Alt string // Alt text fallback }