feat(parse): add ExtractImages stream scanner for OSC 9 markers
Implements ExtractImages() which scans filter stdout line-by-line, extracts valid OSC 9 image markers into ImageRef structs, and replaces marker lines with null-delimited placeholders (\x00IMG:N\x00). Invalid markers (missing path) pass through unchanged. Also clarifies the Index field comment in ImageRef to indicate it is set by ExtractImages, not ParseImageOSC. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
|
|
||||||
// ImageRef represents an image referenced by a filter via OSC 9 marker.
|
// ImageRef represents an image referenced by a filter via OSC 9 marker.
|
||||||
type ImageRef struct {
|
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
|
Path string // Absolute path to image file on disk
|
||||||
Alt string // Alt text fallback
|
Alt string // Alt text fallback
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user