Skip to content

Commit 2eb1fe9

Browse files
committed
refactor(docs): extract markdown write workflow
1 parent 107b9fb commit 2eb1fe9

3 files changed

Lines changed: 466 additions & 61 deletions

File tree

internal/cmd/docs_edit.go

Lines changed: 106 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -245,37 +245,50 @@ func (c *DocsWriteCmd) writePlainTextResult(ctx context.Context, resp *docs.Batc
245245
}
246246

247247
func (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

Comments
 (0)