fix(docs): size markdown images on write
This commit is contained in:
parent
cb817fe65a
commit
e97cad62f6
@ -11,6 +11,7 @@ import (
|
||||
"google.golang.org/api/drive/v3"
|
||||
gapi "google.golang.org/api/googleapi"
|
||||
|
||||
"github.com/steipete/gogcli/internal/config"
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
@ -40,7 +41,11 @@ func (c *DocsWriteCmd) Run(ctx context.Context, kctx *kong.Context, flags *RootF
|
||||
return usage("--append cannot be combined with --replace")
|
||||
}
|
||||
if c.Markdown {
|
||||
return c.writeMarkdown(ctx, flags, id, text)
|
||||
basePath, baseErr := c.markdownBasePath()
|
||||
if baseErr != nil {
|
||||
return baseErr
|
||||
}
|
||||
return c.writeMarkdown(ctx, flags, id, text, basePath)
|
||||
}
|
||||
|
||||
return c.writePlainText(ctx, flags, id, text)
|
||||
@ -60,6 +65,18 @@ func (c *DocsWriteCmd) resolveWriteText(kctx *kong.Context) (string, error) {
|
||||
return text, nil
|
||||
}
|
||||
|
||||
func (c *DocsWriteCmd) markdownBasePath() (string, error) {
|
||||
file := strings.TrimSpace(c.File)
|
||||
if file == "" || file == "-" {
|
||||
return ".", nil
|
||||
}
|
||||
expanded, err := config.ExpandPath(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return expanded, nil
|
||||
}
|
||||
|
||||
func (c *DocsWriteCmd) writePlainText(ctx context.Context, flags *RootFlags, docID, text string) error {
|
||||
svc, err := requireDocsService(ctx, flags)
|
||||
if err != nil {
|
||||
@ -152,7 +169,7 @@ func (c *DocsWriteCmd) writePlainTextResult(ctx context.Context, resp *docs.Batc
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docID, content string) error {
|
||||
func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docID, content string, basePath string) error {
|
||||
u := ui.FromContext(ctx)
|
||||
|
||||
if !c.Replace {
|
||||
@ -165,13 +182,15 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
|
||||
return usage("--markdown cannot be combined with --tab-id")
|
||||
}
|
||||
|
||||
_, driveSvc, err := requireDriveService(ctx, flags)
|
||||
cleaned, images := extractMarkdownImages(content)
|
||||
|
||||
account, driveSvc, err := requireDriveService(ctx, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updated, err := driveSvc.Files.Update(docID, &drive.File{}).
|
||||
Media(strings.NewReader(content), gapi.ContentType(mimeTextMarkdown)).
|
||||
Media(strings.NewReader(cleaned), gapi.ContentType(mimeTextMarkdown)).
|
||||
SupportsAllDrives(true).
|
||||
Fields("id,name,webViewLink").
|
||||
Context(ctx).
|
||||
@ -180,11 +199,21 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
|
||||
return fmt.Errorf("writing markdown to document: %w", err)
|
||||
}
|
||||
|
||||
if c.Pageless {
|
||||
docsSvc, svcErr := requireDocsService(ctx, flags)
|
||||
var docsSvc *docs.Service
|
||||
if len(images) > 0 || c.Pageless {
|
||||
var svcErr error
|
||||
docsSvc, svcErr = newDocsService(ctx, account)
|
||||
if svcErr != nil {
|
||||
return svcErr
|
||||
}
|
||||
}
|
||||
if len(images) > 0 {
|
||||
if err := insertImagesIntoDocs(ctx, account, docsSvc, docID, images, basePath); err != nil {
|
||||
cleanupDocsImagePlaceholders(ctx, docsSvc, docID, images)
|
||||
return fmt.Errorf("insert images: %w", err)
|
||||
}
|
||||
}
|
||||
if c.Pageless {
|
||||
if err := c.applyPageless(ctx, docsSvc, docID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -91,3 +91,230 @@ func TestDocsWrite_MarkdownReplaceUsesDriveUpdate(t *testing.T) {
|
||||
t.Fatalf("expected upload body to contain markdown content, got: %q", uploadBody)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsWrite_MarkdownImagesInsertedAfterDriveUpdate(t *testing.T) {
|
||||
origDocs := newDocsService
|
||||
origDrive := newDriveService
|
||||
t.Cleanup(func() {
|
||||
newDocsService = origDocs
|
||||
newDriveService = origDrive
|
||||
})
|
||||
|
||||
var uploadBody string
|
||||
var sawDocsGet bool
|
||||
var batchReq docs.BatchUpdateDocumentRequest
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/upload/drive/v3/files/doc1"):
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
uploadBody = string(body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "doc1",
|
||||
"name": "Doc",
|
||||
"webViewLink": "https://docs.google.com/document/d/doc1/edit",
|
||||
})
|
||||
return
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/documents/doc1"):
|
||||
sawDocsGet = true
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(docBodyWithText(uploadBody))
|
||||
return
|
||||
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/documents/doc1:batchUpdate"):
|
||||
if err := json.NewDecoder(r.Body).Decode(&batchReq); err != nil {
|
||||
t.Fatalf("decode batch update: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"documentId": "doc1"})
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
driveSvc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/drive/v3/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewDriveService: %v", err)
|
||||
}
|
||||
docsSvc, err := docs.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewDocsService: %v", err)
|
||||
}
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return driveSvc, nil }
|
||||
newDocsService = func(context.Context, string) (*docs.Service, error) { return docsSvc, nil }
|
||||
|
||||
markdown := strings.Join([]string{
|
||||
"# Images",
|
||||
"",
|
||||
"{width=200}",
|
||||
"{width=200 height=150}",
|
||||
"",
|
||||
}, "\n")
|
||||
|
||||
flags := &RootFlags{Account: "a@b.com"}
|
||||
ctx := newDocsJSONContext(t)
|
||||
if err := runKong(t, &DocsWriteCmd{}, []string{"doc1", "--text", markdown, "--replace", "--markdown"}, ctx, flags); err != nil {
|
||||
t.Fatalf("markdown replace write: %v", err)
|
||||
}
|
||||
|
||||
if strings.Contains(uploadBody, "![default]") || strings.Contains(uploadBody, "![wide]") || strings.Contains(uploadBody, "![sized]") {
|
||||
t.Fatalf("expected drive update body to use placeholders, got: %q", uploadBody)
|
||||
}
|
||||
if count := strings.Count(uploadBody, "<<IMG_"); count != 3 {
|
||||
t.Fatalf("expected 3 image placeholders in drive update body, got %d in %q", count, uploadBody)
|
||||
}
|
||||
if !sawDocsGet {
|
||||
t.Fatal("expected image insertion path to read the document")
|
||||
}
|
||||
|
||||
inserts := map[string]*docs.InsertInlineImageRequest{}
|
||||
for _, req := range batchReq.Requests {
|
||||
if req.InsertInlineImage != nil {
|
||||
inserts[req.InsertInlineImage.Uri] = req.InsertInlineImage
|
||||
}
|
||||
}
|
||||
if len(inserts) != 3 {
|
||||
t.Fatalf("expected 3 inserted images, got %d", len(inserts))
|
||||
}
|
||||
|
||||
assertImageSize(t, inserts["https://example.com/default.png"], defaultImageMaxWidthPt, 0)
|
||||
assertImageSize(t, inserts["https://example.com/wide.png"], 200, 0)
|
||||
assertImageSize(t, inserts["https://example.com/sized.png"], 200, 150)
|
||||
}
|
||||
|
||||
func TestDocsWrite_MarkdownLocalImagesResolveRelativeToSourceFile(t *testing.T) {
|
||||
origDocs := newDocsService
|
||||
origDrive := newDriveService
|
||||
t.Cleanup(func() {
|
||||
newDocsService = origDocs
|
||||
newDriveService = origDrive
|
||||
})
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
imgDir := filepath.Join(tmpDir, "assets")
|
||||
if err := os.Mkdir(imgDir, 0o700); err != nil {
|
||||
t.Fatalf("mkdir assets: %v", err)
|
||||
}
|
||||
imagePath := filepath.Join(imgDir, "local.png")
|
||||
if err := os.WriteFile(imagePath, []byte("png"), 0o600); err != nil {
|
||||
t.Fatalf("write image: %v", err)
|
||||
}
|
||||
mdFile := filepath.Join(tmpDir, "source.md")
|
||||
if err := os.WriteFile(mdFile, []byte("\n"), 0o600); err != nil {
|
||||
t.Fatalf("write markdown: %v", err)
|
||||
}
|
||||
|
||||
var uploadBody string
|
||||
var uploadedImageName string
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/upload/drive/v3/files/doc1"):
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read markdown upload body: %v", err)
|
||||
}
|
||||
uploadBody = string(body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": "doc1", "name": "Doc"})
|
||||
return
|
||||
case r.Method == http.MethodGet && strings.Contains(r.URL.Path, "/documents/doc1"):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(docBodyWithText(uploadBody))
|
||||
return
|
||||
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/upload/drive/v3/files"):
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read image upload body: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(body), "png") {
|
||||
t.Fatalf("expected local image file contents in upload body, got %q", string(body))
|
||||
}
|
||||
uploadedImageName = "local.png"
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "img1",
|
||||
"webContentLink": "https://drive.google.com/uc?id=img1",
|
||||
})
|
||||
return
|
||||
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/drive/v3/files/img1/permissions"):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"id": "perm1"})
|
||||
return
|
||||
case r.Method == http.MethodPost && strings.Contains(r.URL.Path, "/documents/doc1:batchUpdate"):
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"documentId": "doc1"})
|
||||
return
|
||||
case r.Method == http.MethodDelete && strings.Contains(r.URL.Path, "/drive/v3/files/img1"):
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
driveSvc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/drive/v3/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewDriveService: %v", err)
|
||||
}
|
||||
docsSvc, err := docs.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewDocsService: %v", err)
|
||||
}
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return driveSvc, nil }
|
||||
newDocsService = func(context.Context, string) (*docs.Service, error) { return docsSvc, nil }
|
||||
|
||||
flags := &RootFlags{Account: "a@b.com"}
|
||||
ctx := newDocsJSONContext(t)
|
||||
if err := runKong(t, &DocsWriteCmd{}, []string{"doc1", "--file", mdFile, "--replace", "--markdown"}, ctx, flags); err != nil {
|
||||
t.Fatalf("markdown replace write: %v", err)
|
||||
}
|
||||
if uploadedImageName != "local.png" {
|
||||
t.Fatalf("expected local image upload from markdown directory, got %q", uploadedImageName)
|
||||
}
|
||||
}
|
||||
|
||||
func assertImageSize(t *testing.T, ins *docs.InsertInlineImageRequest, wantWidth, wantHeight float64) {
|
||||
t.Helper()
|
||||
if ins == nil {
|
||||
t.Fatal("missing inserted image request")
|
||||
}
|
||||
if wantWidth == 0 {
|
||||
if ins.ObjectSize.Width != nil {
|
||||
t.Fatalf("expected no width, got %+v", ins.ObjectSize.Width)
|
||||
}
|
||||
} else if ins.ObjectSize.Width == nil || ins.ObjectSize.Width.Magnitude != wantWidth || ins.ObjectSize.Width.Unit != "PT" {
|
||||
t.Fatalf("expected width=%v PT, got %+v", wantWidth, ins.ObjectSize.Width)
|
||||
}
|
||||
if wantHeight == 0 {
|
||||
if ins.ObjectSize.Height != nil {
|
||||
t.Fatalf("expected no height, got %+v", ins.ObjectSize.Height)
|
||||
}
|
||||
} else if ins.ObjectSize.Height == nil || ins.ObjectSize.Height.Magnitude != wantHeight || ins.ObjectSize.Height.Unit != "PT" {
|
||||
t.Fatalf("expected height=%v PT, got %+v", wantHeight, ins.ObjectSize.Height)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user