@@ -24,7 +24,9 @@ type DocsInsertImageCmd struct {
2424 DocID string `arg:"" name:"docId" help:"Doc ID"`
2525 File string `name:"file" help:"Local PNG, JPEG, or GIF image to upload and insert" type:"existingfile"`
2626 URL string `name:"url" help:"Public HTTPS image URL to insert directly"`
27- At string `name:"at" help:"Placeholder text to replace, or 'end' to append" default:"end"`
27+ At * string `name:"at" help:"Placeholder text to delete and replace, or 'end' to append"`
28+ Before * string `name:"before" help:"Insert before the first literal text match without deleting it"`
29+ After * string `name:"after" help:"Insert after the first literal text match without deleting it"`
2830 Width float64 `name:"width" help:"Image width in points; default 468pt" default:"468"`
2931 Height float64 `name:"height" help:"Image height in points (optional; width-only preserves aspect ratio)"`
3032 Parent string `name:"parent" help:"Drive folder ID for the uploaded image"`
@@ -53,6 +55,19 @@ type docsInsertImageSource struct {
5355 imageURL string
5456}
5557
58+ type docsImageAnchorMode string
59+
60+ const (
61+ docsImageAnchorReplace docsImageAnchorMode = "at"
62+ docsImageAnchorBefore docsImageAnchorMode = "before"
63+ docsImageAnchorAfter docsImageAnchorMode = "after"
64+ )
65+
66+ type docsImageTarget struct {
67+ anchor string
68+ mode docsImageAnchorMode
69+ }
70+
5671func (c * DocsInsertImageCmd ) Run (ctx context.Context , flags * RootFlags ) error {
5772 docID := strings .TrimSpace (c .DocID )
5873 if docID == "" {
@@ -65,16 +80,16 @@ func (c *DocsInsertImageCmd) Run(ctx context.Context, flags *RootFlags) error {
6580 if err != nil {
6681 return err
6782 }
68- at := strings . TrimSpace ( c . At )
69- if at == "" {
70- return usage ( "empty --at" )
83+ target , err := c . resolveTarget ( )
84+ if err != nil {
85+ return err
7186 }
7287 dryRunPayload := map [string ]any {
73- "documentId" : docID ,
74- "at " : at ,
75- "width " : c . Width ,
76- "height " : c . Height ,
77- "tab" : c . Tab ,
88+ "documentId" : docID ,
89+ "width " : c . Width ,
90+ "height " : c . Height ,
91+ "tab " : c . Tab ,
92+ string ( target . mode ): target . anchor ,
7893 }
7994 if source .imageURL != "" {
8095 dryRunPayload ["url" ] = source .imageURL
@@ -105,20 +120,48 @@ func (c *DocsInsertImageCmd) Run(ctx context.Context, flags *RootFlags) error {
105120
106121 var result docsInsertImageResult
107122 if source .imageURL != "" {
108- result , err = c .runURL (ctx , docsSvc , docID , source .imageURL , at )
123+ result , err = c .runURL (ctx , docsSvc , docID , source .imageURL , target )
109124 } else {
110125 driveSvc , driveErr := driveService (ctx , account )
111126 if driveErr != nil {
112127 return driveErr
113128 }
114- result , err = c .runFile (ctx , docsSvc , driveSvc , docID , source .localPath , source .name , source .mimeType , at )
129+ result , err = c .runFile (ctx , docsSvc , driveSvc , docID , source .localPath , source .name , source .mimeType , target )
115130 }
116131 if err != nil {
117132 return err
118133 }
119134 return writeDocsInsertImageResult (ctx , result )
120135}
121136
137+ func (c * DocsInsertImageCmd ) resolveTarget () (docsImageTarget , error ) {
138+ set := 0
139+ for _ , value := range []* string {c .At , c .Before , c .After } {
140+ if value != nil {
141+ set ++
142+ }
143+ }
144+ if set > 1 {
145+ return docsImageTarget {}, usage ("--at, --before, and --after are mutually exclusive" )
146+ }
147+
148+ target := docsImageTarget {anchor : docsAtIndexEnd , mode : docsImageAnchorReplace }
149+ switch {
150+ case c .At != nil :
151+ target .anchor = strings .TrimSpace (* c .At )
152+ case c .Before != nil :
153+ target .anchor = strings .TrimSpace (* c .Before )
154+ target .mode = docsImageAnchorBefore
155+ case c .After != nil :
156+ target .anchor = strings .TrimSpace (* c .After )
157+ target .mode = docsImageAnchorAfter
158+ }
159+ if target .anchor == "" {
160+ return docsImageTarget {}, usage (fmt .Sprintf ("empty --%s" , target .mode ))
161+ }
162+ return target , nil
163+ }
164+
122165func (c * DocsInsertImageCmd ) resolveSource () (docsInsertImageSource , error ) {
123166 localFile := strings .TrimSpace (c .File )
124167 imageURL := strings .TrimSpace (c .URL )
@@ -198,12 +241,12 @@ func writeDocsInsertImageResult(ctx context.Context, result docsInsertImageResul
198241 return nil
199242}
200243
201- func (c * DocsInsertImageCmd ) runURL (ctx context.Context , docsSvc * docs.Service , docID , imageURL , at string ) (docsInsertImageResult , error ) {
244+ func (c * DocsInsertImageCmd ) runURL (ctx context.Context , docsSvc * docs.Service , docID , imageURL string , target docsImageTarget ) (docsInsertImageResult , error ) {
202245 result := docsInsertImageResult {sourceURL : imageURL }
203- return c .insertImageURL (ctx , docsSvc , docID , imageURL , at , result )
246+ return c .insertImageURL (ctx , docsSvc , docID , imageURL , target , result )
204247}
205248
206- func (c * DocsInsertImageCmd ) runFile (ctx context.Context , docsSvc * docs.Service , driveSvc * drive.Service , docID , localPath , name , mimeType , at string ) (result docsInsertImageResult , err error ) {
249+ func (c * DocsInsertImageCmd ) runFile (ctx context.Context , docsSvc * docs.Service , driveSvc * drive.Service , docID , localPath , name , mimeType string , target docsImageTarget ) (result docsInsertImageResult , err error ) {
207250 uploaded , err := uploadDocsInlineImage (ctx , driveSvc , localPath , name , mimeType , strings .TrimSpace (c .Parent ))
208251 if err != nil {
209252 return result , err
@@ -218,7 +261,7 @@ func (c *DocsInsertImageCmd) runFile(ctx context.Context, docsSvc *docs.Service,
218261 Do ()
219262 if err != nil {
220263 if strings .EqualFold (c .OnRestricted , "link" ) && isDrivePublicSharingRestricted (err ) {
221- return c .insertRestrictedImageFallback (ctx , docsSvc , docID , uploaded , at , result )
264+ return c .insertRestrictedImageFallback (ctx , docsSvc , docID , uploaded , target , result )
222265 }
223266 return result , fmt .Errorf ("share uploaded image publicly: %w" , err )
224267 }
@@ -243,11 +286,11 @@ func (c *DocsInsertImageCmd) runFile(ctx context.Context, docsSvc *docs.Service,
243286 }()
244287
245288 imageURL := driveImageDownloadURL (uploaded .Id )
246- return c .insertImageURL (ctx , docsSvc , docID , imageURL , at , result )
289+ return c .insertImageURL (ctx , docsSvc , docID , imageURL , target , result )
247290}
248291
249- func (c * DocsInsertImageCmd ) insertImageURL (ctx context.Context , docsSvc * docs.Service , docID , imageURL , at string , result docsInsertImageResult ) (docsInsertImageResult , error ) {
250- reqs , index , tabID , err := c .buildInsertRequests (ctx , docsSvc , docID , at , imageURL )
292+ func (c * DocsInsertImageCmd ) insertImageURL (ctx context.Context , docsSvc * docs.Service , docID , imageURL string , target docsImageTarget , result docsInsertImageResult ) (docsInsertImageResult , error ) {
293+ reqs , index , tabID , err := c .buildInsertRequests (ctx , docsSvc , docID , target , imageURL )
251294 if err != nil {
252295 return result , err
253296 }
@@ -261,12 +304,12 @@ func (c *DocsInsertImageCmd) insertImageURL(ctx context.Context, docsSvc *docs.S
261304 return result , nil
262305}
263306
264- func (c * DocsInsertImageCmd ) insertRestrictedImageFallback (ctx context.Context , docsSvc * docs.Service , docID string , uploaded * drive.File , at string , result docsInsertImageResult ) (docsInsertImageResult , error ) {
307+ func (c * DocsInsertImageCmd ) insertRestrictedImageFallback (ctx context.Context , docsSvc * docs.Service , docID string , uploaded * drive.File , target docsImageTarget , result docsInsertImageResult ) (docsInsertImageResult , error ) {
265308 link := uploaded .WebViewLink
266309 if link == "" {
267310 link = bestEffortWebURL ("drive" , uploaded .Id )
268311 }
269- reqs , index , tabID , err := c .buildLinkFallbackRequests (ctx , docsSvc , docID , at , link )
312+ reqs , index , tabID , err := c .buildLinkFallbackRequests (ctx , docsSvc , docID , target , link )
270313 if err != nil {
271314 return result , err
272315 }
@@ -305,8 +348,8 @@ func uploadDocsInlineImage(ctx context.Context, svc *drive.Service, localPath, n
305348 return created , nil
306349}
307350
308- func (c * DocsInsertImageCmd ) buildInsertRequests (ctx context.Context , svc * docs.Service , docID , at , imageURL string ) ([]* docs.Request , int64 , string , error ) {
309- index , placeholder , tabID , err := c .resolveImageTarget (ctx , svc , docID , at )
351+ func (c * DocsInsertImageCmd ) buildInsertRequests (ctx context.Context , svc * docs.Service , docID string , target docsImageTarget , imageURL string ) ([]* docs.Request , int64 , string , error ) {
352+ index , placeholder , tabID , err := c .resolveImageTarget (ctx , svc , docID , target )
310353 if err != nil {
311354 return nil , 0 , "" , err
312355 }
@@ -331,8 +374,8 @@ func (c *DocsInsertImageCmd) buildInsertRequests(ctx context.Context, svc *docs.
331374 return reqs , index , tabID , nil
332375}
333376
334- func (c * DocsInsertImageCmd ) buildLinkFallbackRequests (ctx context.Context , svc * docs.Service , docID , at , link string ) ([]* docs.Request , int64 , string , error ) {
335- index , placeholder , tabID , err := c .resolveImageTarget (ctx , svc , docID , at )
377+ func (c * DocsInsertImageCmd ) buildLinkFallbackRequests (ctx context.Context , svc * docs.Service , docID string , target docsImageTarget , link string ) ([]* docs.Request , int64 , string , error ) {
378+ index , placeholder , tabID , err := c .resolveImageTarget (ctx , svc , docID , target )
336379 if err != nil {
337380 return nil , 0 , "" , err
338381 }
@@ -349,8 +392,8 @@ func (c *DocsInsertImageCmd) buildLinkFallbackRequests(ctx context.Context, svc
349392 return reqs , index , tabID , nil
350393}
351394
352- func (c * DocsInsertImageCmd ) resolveImageTarget (ctx context.Context , svc * docs.Service , docID , at string ) (int64 , * docsedit.TextRange , string , error ) {
353- if strings .EqualFold (at , docsAtIndexEnd ) {
395+ func (c * DocsInsertImageCmd ) resolveImageTarget (ctx context.Context , svc * docs.Service , docID string , target docsImageTarget ) (int64 , * docsedit.TextRange , string , error ) {
396+ if target . mode == docsImageAnchorReplace && strings .EqualFold (target . anchor , docsAtIndexEnd ) {
354397 endIndex , tabID , err := docsTargetEndIndexAndTabID (ctx , svc , docID , c .Tab )
355398 if err != nil {
356399 return 0 , nil , "" , err
@@ -361,15 +404,23 @@ func (c *DocsInsertImageCmd) resolveImageTarget(ctx context.Context, svc *docs.S
361404 if err != nil {
362405 return 0 , nil , "" , err
363406 }
364- matches := docsedit .FindTextRanges (loaded .target , at , docsedit.SearchOptions {
407+ matches := docsedit .FindTextRanges (loaded .target , target . anchor , docsedit.SearchOptions {
365408 MatchCase : true ,
366409 PreserveHTMLEntities : true ,
367410 RequireTextSegment : true ,
368411 })
369412 if len (matches ) == 0 {
370- return 0 , nil , "" , fmt .Errorf ("placeholder not found: %q" , at )
413+ return 0 , nil , "" , fmt .Errorf ("anchor not found: %q" , target .anchor )
414+ }
415+ match := matches [0 ]
416+ switch target .mode {
417+ case docsImageAnchorBefore :
418+ return match .StartIndex , nil , loaded .tabID , nil
419+ case docsImageAnchorAfter :
420+ return match .EndIndex , nil , loaded .tabID , nil
421+ default :
422+ return match .StartIndex , & match , loaded .tabID , nil
371423 }
372- return matches [0 ].StartIndex , & matches [0 ], loaded .tabID , nil
373424}
374425
375426func isDocsInsertImageMime (mimeType string ) bool {
0 commit comments