@@ -245,37 +245,50 @@ func (c *DocsWriteCmd) writePlainTextResult(ctx context.Context, resp *docs.Batc
245245}
246246
247247func (c * DocsWriteCmd ) writeMarkdown (ctx context.Context , flags * RootFlags , docID , content string ) error {
248- u := ui .FromContext (ctx )
249-
250- if c .Append {
251- return c .appendMarkdown (ctx , flags , docID , content )
252- }
253- if ! c .Replace {
254- return usage ("--markdown requires --replace or --append" )
255- }
256- // Drive's markdown converter operates on entire documents, so we cannot use
257- // the Drive Files.Update path when --tab is set. Instead, render markdown
258- // locally and apply it to the specified tab via Docs batchUpdate.
259- if c .Tab != "" {
260- return c .replaceMarkdownInTab (ctx , flags , docID , content )
248+ cleaned , images := extractMarkdownImages (content )
249+ plan , err := docsedit .PlanMarkdownWrite (docsedit.MarkdownWriteOptions {
250+ Markdown : cleaned ,
251+ ImageCount : len (images ),
252+ Append : c .Append ,
253+ Replace : c .Replace ,
254+ Tab : c .Tab ,
255+ CheckOrphans : c .CheckOrphans ,
256+ ApplyDocumentStyle : c .Pageless || c .Layout .any (),
257+ })
258+ if err != nil {
259+ return usage (err .Error ())
261260 }
262261
263- cleaned , images := extractMarkdownImages (content )
264- if docsmarkdown .HasTableCellBreaks (cleaned ) {
265- return c .replaceMarkdownInTab (ctx , flags , docID , content )
262+ switch plan .Mode {
263+ case docsedit .MarkdownWriteDriveReplace :
264+ return c .replaceMarkdownWithDrive (ctx , flags , docID , content , images , plan )
265+ case docsedit .MarkdownWriteLocalAppend :
266+ return c .appendMarkdown (ctx , flags , docID , content , plan )
267+ case docsedit .MarkdownWriteLocalReplace :
268+ return c .replaceMarkdownLocally (ctx , flags , docID , content , plan )
269+ default :
270+ return fmt .Errorf ("unsupported markdown write mode: %d" , plan .Mode )
266271 }
267- cleaned = docsmarkdown .NormalizeTablesForDriveImport (cleaned )
268- explicitHeadingAnchors := docsmarkdown .ImportExplicitHeadingAnchors (cleaned )
269- cleaned = docsmarkdown .StripHeadingAnchors (cleaned )
272+ }
273+
274+ func (c * DocsWriteCmd ) replaceMarkdownWithDrive (
275+ ctx context.Context ,
276+ flags * RootFlags ,
277+ docID string ,
278+ content string ,
279+ images []markdownImage ,
280+ plan docsedit.MarkdownWritePlan ,
281+ ) error {
282+ u := ui .FromContext (ctx )
270283 dryRunPayload := map [string ]any {
271284 "document_id" : docID ,
272285 "written" : len (content ),
273286 "append" : false ,
274287 "replace" : true ,
275288 "markdown" : true ,
276289 "pageless" : c .Pageless ,
277- "images" : len ( images ) ,
278- "check_orphans" : c .CheckOrphans ,
290+ "images" : plan . ImageCount ,
291+ "check_orphans" : plan .CheckOrphans ,
279292 }
280293 for k , v := range c .Layout .dryRunPayload () {
281294 dryRunPayload [k ] = v
@@ -290,12 +303,20 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
290303 }
291304
292305 var docsSvc * docs.Service
293- if c .CheckOrphans {
306+ if plan .CheckOrphans {
294307 docsSvc , err = docsService (ctx , account )
295308 if err != nil {
296309 return err
297310 }
298- orphans , tabID , orphanErr := findDocsWriteMarkdownOrphans (ctx , driveSvc , docsSvc , docID , content , "" , true )
311+ orphans , tabID , orphanErr := findDocsWriteMarkdownOrphans (
312+ ctx ,
313+ driveSvc ,
314+ docsSvc ,
315+ docID ,
316+ content ,
317+ plan .Tab ,
318+ plan .OrphanScopeWholeDocument ,
319+ )
299320 if orphanErr != nil {
300321 return orphanErr
301322 }
@@ -305,7 +326,7 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
305326 }
306327
307328 updated , err := driveSvc .Files .Update (docID , & drive.File {}).
308- Media (strings .NewReader (cleaned ), gapi .ContentType (mimeTextMarkdown )).
329+ Media (strings .NewReader (plan . Markdown ), gapi .ContentType (mimeTextMarkdown )).
309330 SupportsAllDrives (true ).
310331 Fields ("id,name,webViewLink" ).
311332 Context (ctx ).
@@ -314,29 +335,28 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
314335 return fmt .Errorf ("writing markdown to document: %w" , err )
315336 }
316337
317- needsDocsSvc := len (images ) > 0 || c .Pageless || c .Layout .any () || markdownMayContainHeadingLinks (cleaned )
318- if needsDocsSvc && docsSvc == nil {
338+ if plan .RequiresDocumentsService && docsSvc == nil {
319339 var svcErr error
320340 docsSvc , svcErr = docsService (ctx , account )
321341 if svcErr != nil {
322342 return svcErr
323343 }
324344 }
325345 rewrittenHeadingLinks := 0
326- if markdownMayContainHeadingLinks ( cleaned ) {
327- count , rewriteErr := rewriteMarkdownHeadingLinks (ctx , docsSvc , docID , "" , explicitHeadingAnchors )
346+ if plan . RewriteHeadingLinks {
347+ count , rewriteErr := rewriteMarkdownHeadingLinks (ctx , docsSvc , docID , plan . Tab , plan . ExplicitHeadingAnchors )
328348 if rewriteErr != nil {
329349 return fmt .Errorf ("rewrite heading links: %w" , rewriteErr )
330350 }
331351 rewrittenHeadingLinks = count
332352 }
333- if len ( images ) > 0 {
334- if err := insertImagesIntoDocs (ctx , docsSvc , docID , images , "" ); err != nil {
335- cleanupDocsImagePlaceholders (ctx , docsSvc , docID , images , "" )
353+ if plan . InsertImages {
354+ if err := insertImagesIntoDocs (ctx , docsSvc , docID , images , plan . Tab ); err != nil {
355+ cleanupDocsImagePlaceholders (ctx , docsSvc , docID , images , plan . Tab )
336356 return fmt .Errorf ("insert images: %w" , err )
337357 }
338358 }
339- if c . Pageless || c . Layout . any () {
359+ if plan . ApplyDocumentStyle {
340360 if err := c .applyDocumentStyle (ctx , docsSvc , docID ); err != nil {
341361 return err
342362 }
@@ -373,18 +393,22 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
373393 return nil
374394}
375395
376- func (c * DocsWriteCmd ) appendMarkdown (ctx context.Context , flags * RootFlags , docID , content string ) error {
377- cleaned , images := extractMarkdownImages (content )
378- explicitHeadingAnchors := docsmarkdown .ExplicitHeadingAnchors (cleaned )
396+ func (c * DocsWriteCmd ) appendMarkdown (
397+ ctx context.Context ,
398+ flags * RootFlags ,
399+ docID string ,
400+ content string ,
401+ plan docsedit.MarkdownWritePlan ,
402+ ) error {
379403 dryRunPayload := map [string ]any {
380404 "document_id" : docID ,
381- "written" : len (cleaned ),
405+ "written" : len (plan . Markdown ),
382406 "append" : true ,
383407 "replace" : false ,
384408 "markdown" : true ,
385409 "pageless" : c .Pageless ,
386- "tab" : c .Tab ,
387- "images" : len ( images ) ,
410+ "tab" : plan .Tab ,
411+ "images" : plan . ImageCount ,
388412 }
389413 for k , v := range c .Layout .dryRunPayload () {
390414 dryRunPayload [k ] = v
@@ -405,7 +429,7 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
405429 c .Tab = tabID
406430 insertIndex := docsedit .AppendIndex (endIndex )
407431 insertedMarkdownStart := insertIndex
408- appendElements := docsmarkdown .ParseMarkdown (cleaned )
432+ appendElements := docsmarkdown .ParseMarkdown (plan . Markdown )
409433 if insertIndex > 1 && markdownAppendNeedsParagraphBoundary (appendElements ) {
410434 insertedMarkdownStart ++
411435 }
@@ -417,12 +441,21 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
417441 }
418442 return err
419443 }
420- if err := c .applyDocumentStyle (ctx , svc , docID ); err != nil {
421- return err
444+ if plan .ApplyDocumentStyle {
445+ if err := c .applyDocumentStyle (ctx , svc , docID ); err != nil {
446+ return err
447+ }
422448 }
423449 rewrittenHeadingLinks := 0
424- if markdownMayContainHeadingLinks (cleaned ) {
425- count , rewriteErr := rewriteMarkdownHeadingLinksFromIndex (ctx , svc , docID , c .Tab , explicitHeadingAnchors , insertedMarkdownStart )
450+ if plan .RewriteHeadingLinks {
451+ count , rewriteErr := rewriteMarkdownHeadingLinksFromIndex (
452+ ctx ,
453+ svc ,
454+ docID ,
455+ c .Tab ,
456+ plan .ExplicitHeadingAnchors ,
457+ insertedMarkdownStart ,
458+ )
426459 if rewriteErr != nil {
427460 return fmt .Errorf ("rewrite heading links: %w" , rewriteErr )
428461 }
@@ -466,24 +499,26 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
466499 return nil
467500}
468501
469- // replaceMarkdownInTab implements --replace --markdown --tab=<tab>. Drive's
470- // markdown converter is whole-document-only, so per-tab whole-tab re-render
471- // is achieved at the gogcli layer: render markdown locally with the same
472- // Docs API path used by --append --markdown, after wiping the tab's existing
473- // body content via DeleteContentRange. Other tabs are untouched.
474- func (c * DocsWriteCmd ) replaceMarkdownInTab (ctx context.Context , flags * RootFlags , docID , content string ) error {
475- cleaned , images := extractMarkdownImages (content )
476- explicitHeadingAnchors := docsmarkdown .ExplicitHeadingAnchors (cleaned )
502+ // replaceMarkdownLocally renders Markdown through Docs batchUpdate after
503+ // clearing the selected body. This preserves tab targeting and table-cell
504+ // line breaks that Drive's whole-document converter cannot represent.
505+ func (c * DocsWriteCmd ) replaceMarkdownLocally (
506+ ctx context.Context ,
507+ flags * RootFlags ,
508+ docID string ,
509+ content string ,
510+ plan docsedit.MarkdownWritePlan ,
511+ ) error {
477512 dryRunPayload := map [string ]any {
478513 "document_id" : docID ,
479- "written" : len (cleaned ),
514+ "written" : len (plan . Markdown ),
480515 "append" : false ,
481516 "replace" : true ,
482517 "markdown" : true ,
483518 "pageless" : c .Pageless ,
484- "tab" : c .Tab ,
485- "images" : len ( images ) ,
486- "check_orphans" : c .CheckOrphans ,
519+ "tab" : plan .Tab ,
520+ "images" : plan . ImageCount ,
521+ "check_orphans" : plan .CheckOrphans ,
487522 }
488523 for k , v := range c .Layout .dryRunPayload () {
489524 dryRunPayload [k ] = v
@@ -494,7 +529,7 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
494529
495530 var svc * docs.Service
496531 var err error
497- if c .CheckOrphans {
532+ if plan .CheckOrphans {
498533 account , driveSvc , driveErr := requireDriveService (ctx , flags )
499534 if driveErr != nil {
500535 return driveErr
@@ -503,7 +538,15 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
503538 if err != nil {
504539 return err
505540 }
506- orphans , resolvedTabID , orphanErr := findDocsWriteMarkdownOrphans (ctx , driveSvc , svc , docID , content , c .Tab , false )
541+ orphans , resolvedTabID , orphanErr := findDocsWriteMarkdownOrphans (
542+ ctx ,
543+ driveSvc ,
544+ svc ,
545+ docID ,
546+ content ,
547+ plan .Tab ,
548+ plan .OrphanScopeWholeDocument ,
549+ )
507550 if orphanErr != nil {
508551 return orphanErr
509552 }
@@ -550,12 +593,14 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
550593 }
551594 return err
552595 }
553- if err := c .applyDocumentStyle (ctx , svc , docID ); err != nil {
554- return err
596+ if plan .ApplyDocumentStyle {
597+ if err := c .applyDocumentStyle (ctx , svc , docID ); err != nil {
598+ return err
599+ }
555600 }
556601 rewrittenHeadingLinks := 0
557- if markdownMayContainHeadingLinks ( cleaned ) {
558- count , rewriteErr := rewriteMarkdownHeadingLinks (ctx , svc , docID , tabID , explicitHeadingAnchors )
602+ if plan . RewriteHeadingLinks {
603+ count , rewriteErr := rewriteMarkdownHeadingLinks (ctx , svc , docID , tabID , plan . ExplicitHeadingAnchors )
559604 if rewriteErr != nil {
560605 return fmt .Errorf ("rewrite heading links: %w" , rewriteErr )
561606 }
0 commit comments