Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

- Docs: preserve nested list levels when writing markdown into a specific tab with `docs write --replace --markdown --tab`. (#696)
- Docs: fix `docs export --tab` tab resolution against the live Docs API field mask. (#696)
- Docs: strip Pandoc-style explicit heading anchors like `{#slug}` from rendered markdown headings and resolve matching same-document links. (#703)
- Docs: render GFM `~~strikethrough~~` spans in the local markdown writer used by `docs write --tab --markdown`. (#702)
- Docs: batch table-cell writes for `docs write --tab --markdown` to avoid per-cell Docs API quota bursts on table-heavy documents. (#699) — thanks @sebsnyk.
- Gmail: preserve existing `gmail drafts update` attachments when `--attach` is omitted, add `--clear-attachments` for intentional removal, and keep `--attach` as explicit replacement. (#680, #681) — thanks @chrischall.
Expand Down
61 changes: 57 additions & 4 deletions internal/cmd/docs_edit.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,8 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI

cleaned, images := extractMarkdownImages(content)
cleaned = normalizeMarkdownTablesForDriveImport(cleaned)
explicitHeadingAnchors := markdownImportExplicitHeadingAnchors(cleaned)
cleaned = stripMarkdownHeadingAnchors(cleaned)
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(content),
Expand Down Expand Up @@ -305,7 +307,7 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI
}
rewrittenHeadingLinks := 0
if markdownMayContainHeadingLinks(cleaned) {
count, rewriteErr := rewriteMarkdownHeadingLinks(ctx, docsSvc, docID)
count, rewriteErr := rewriteMarkdownHeadingLinks(ctx, docsSvc, docID, "", explicitHeadingAnchors)
if rewriteErr != nil {
return fmt.Errorf("rewrite heading links: %w", rewriteErr)
}
Expand Down Expand Up @@ -356,6 +358,7 @@ func (c *DocsWriteCmd) writeMarkdown(ctx context.Context, flags *RootFlags, docI

func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, docID, content string) error {
cleaned, images := extractMarkdownImages(content)
explicitHeadingAnchors := markdownExplicitHeadingAnchors(cleaned)
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(cleaned),
Expand Down Expand Up @@ -384,8 +387,13 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
}
c.Tab = tabID
insertIndex := docsAppendIndex(endIndex)
insertedMarkdownStart := insertIndex
appendElements := ParseMarkdown(cleaned)
if insertIndex > 1 && markdownAppendNeedsParagraphBoundary(appendElements) {
insertedMarkdownStart++
}

requestCount, inserted, err := insertDocsMarkdownAt(ctx, svc, docID, insertIndex, content, c.Tab)
requestCount, inserted, err := insertDocsMarkdownAtWithOptions(ctx, svc, docID, insertIndex, content, c.Tab, true)
if err != nil {
if isDocsNotFound(err) {
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", docID)
Expand All @@ -395,6 +403,15 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
if err := c.applyDocumentStyle(ctx, svc, docID); err != nil {
return err
}
rewrittenHeadingLinks := 0
if markdownMayContainHeadingLinks(cleaned) {
count, rewriteErr := rewriteMarkdownHeadingLinksFromIndex(ctx, svc, docID, c.Tab, explicitHeadingAnchors, insertedMarkdownStart)
if rewriteErr != nil {
return fmt.Errorf("rewrite heading links: %w", rewriteErr)
}
rewrittenHeadingLinks = count
requestCount += count
}

if outfmt.IsJSON(ctx) {
payload := map[string]any{
Expand All @@ -408,6 +425,9 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
if c.Pageless {
payload["pageless"] = true
}
if rewrittenHeadingLinks > 0 {
payload["headingLinks"] = rewrittenHeadingLinks
}
for k, v := range c.Layout.dryRunPayload() {
payload[k] = v
}
Expand All @@ -420,6 +440,9 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
u.Out().Linef("requests\t%d", requestCount)
u.Out().Linef("mode\tappended (markdown converted)")
u.Out().Linef("index\t%d", insertIndex)
if rewrittenHeadingLinks > 0 {
u.Out().Linef("headingLinks\t%d", rewrittenHeadingLinks)
}
if c.Pageless {
u.Out().Linef("pageless\ttrue")
}
Expand All @@ -433,6 +456,7 @@ func (c *DocsWriteCmd) appendMarkdown(ctx context.Context, flags *RootFlags, doc
// body content via DeleteContentRange. Other tabs are untouched.
func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlags, docID, content string) error {
cleaned, images := extractMarkdownImages(content)
explicitHeadingAnchors := markdownExplicitHeadingAnchors(cleaned)
dryRunPayload := map[string]any{
"document_id": docID,
"written": len(cleaned),
Expand Down Expand Up @@ -480,7 +504,7 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
}
}

requestCount, inserted, err := insertDocsMarkdownAt(ctx, svc, docID, 1, content, tabID)
requestCount, inserted, err := insertDocsMarkdownAtWithOptions(ctx, svc, docID, 1, content, tabID, true)
if err != nil {
if isDocsNotFound(err) {
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", docID)
Expand All @@ -490,6 +514,15 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
if err := c.applyDocumentStyle(ctx, svc, docID); err != nil {
return err
}
rewrittenHeadingLinks := 0
if markdownMayContainHeadingLinks(cleaned) {
count, rewriteErr := rewriteMarkdownHeadingLinks(ctx, svc, docID, tabID, explicitHeadingAnchors)
if rewriteErr != nil {
return fmt.Errorf("rewrite heading links: %w", rewriteErr)
}
rewrittenHeadingLinks = count
requestCount += count
}

if outfmt.IsJSON(ctx) {
payload := map[string]any{
Expand All @@ -503,6 +536,9 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
if c.Pageless {
payload["pageless"] = true
}
if rewrittenHeadingLinks > 0 {
payload["headingLinks"] = rewrittenHeadingLinks
}
for k, v := range c.Layout.dryRunPayload() {
payload[k] = v
}
Expand All @@ -515,6 +551,9 @@ func (c *DocsWriteCmd) replaceMarkdownInTab(ctx context.Context, flags *RootFlag
u.Out().Linef("requests\t%d", requestCount)
u.Out().Linef("mode\treplaced tab (markdown converted)")
u.Out().Linef("tabId\t%s", tabID)
if rewrittenHeadingLinks > 0 {
u.Out().Linef("headingLinks\t%d", rewrittenHeadingLinks)
}
if c.Pageless {
u.Out().Linef("pageless\ttrue")
}
Expand Down Expand Up @@ -626,6 +665,8 @@ func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *Root

if c.Markdown {
var inserted int
cleaned, _ := extractMarkdownImages(text)
explicitHeadingAnchors := markdownExplicitHeadingAnchors(cleaned)
if replacing {
loaded, loadErr := loadDocsTargetDocument(ctx, svc, id, c.Tab)
if loadErr != nil {
Expand All @@ -640,7 +681,19 @@ func (c *DocsUpdateCmd) Run(ctx context.Context, kctx *kong.Context, flags *Root
requestCount = replacedRequests
}
} else {
requestCount, inserted, err = insertDocsMarkdownAt(ctx, svc, id, insertIndex, text, c.Tab)
insertedMarkdownStart := insertIndex
insertElements := ParseMarkdown(cleaned)
stripMarkdownElementHeadingAnchors(insertElements)
if insertIndex > 1 && markdownAppendNeedsParagraphBoundary(insertElements) {
insertedMarkdownStart++
}
var insertedMarkdownEnd int64
requestCount, inserted, insertedMarkdownEnd, err = insertDocsMarkdownAtWithOptionsAndEnd(ctx, svc, id, insertIndex, text, c.Tab, true)
if err == nil && markdownMayContainHeadingLinks(cleaned) {
var rewritten int
rewritten, err = rewriteMarkdownHeadingLinksInRange(ctx, svc, id, c.Tab, explicitHeadingAnchors, insertedMarkdownStart, insertedMarkdownEnd)
requestCount += rewritten
}
}
if err != nil {
if isDocsNotFound(err) {
Expand Down
28 changes: 28 additions & 0 deletions internal/cmd/docs_formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,34 @@ func TestMarkdownToDocsRequests_Strikethrough(t *testing.T) {
}
}

func TestMarkdownToDocsRequests_StripsExplicitHeadingAnchor(t *testing.T) {
elements := ParseMarkdown("## Files {#attachments}\n")
stripMarkdownElementHeadingAnchors(elements)
requests, text, tables := MarkdownToDocsRequests(elements, 5, "t.second")
if text != "Files\n" {
t.Fatalf("text = %q, want %q", text, "Files\n")
}
if len(tables) != 0 {
t.Fatalf("unexpected tables: %d", len(tables))
}
if len(requests) == 0 || requests[0].UpdateParagraphStyle == nil {
t.Fatalf("expected heading paragraph style request, got %#v", requests)
}
if got := requests[0].UpdateParagraphStyle.Range; got.StartIndex != 5 || got.EndIndex != 11 || got.TabId != "t.second" {
t.Fatalf("unexpected heading range: %#v", got)
}
}

func TestMarkdownToDocsRequests_KeepsExplicitHeadingAnchorWithoutOptIn(t *testing.T) {
_, text, tables := MarkdownToDocsRequests(ParseMarkdown("## Files {#attachments}\n"), 5, "")
if text != "Files {#attachments}\n" {
t.Fatalf("text = %q, want explicit anchor preserved", text)
}
if len(tables) != 0 {
t.Fatalf("unexpected tables: %d", len(tables))
}
}

func TestMarkdownToDocsRequests_NestedLists(t *testing.T) {
elements := ParseMarkdown("- Parent\n - **Child**\n - Grandchild\n\n1. One\n 1. Nested one")
requests, text, tables := MarkdownToDocsRequests(elements, 10, "t.second")
Expand Down
14 changes: 14 additions & 0 deletions internal/cmd/docs_import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,20 @@ func TestMarkdownImage_Placeholder(t *testing.T) {
}
}

func TestSubtractMarkdownImagePlaceholderDrift(t *testing.T) {
images := []markdownImage{
{token: "test", index: 0},
{token: "test", index: 1},
}
placeholderDrift := (utf16Len("<<IMG_test_0>>") - 1) + (utf16Len("<<IMG_test_1>>") - 1)
if got, want := subtractMarkdownImagePlaceholderDrift(40, 1, images), int64(40)-placeholderDrift; got != want {
t.Fatalf("subtractMarkdownImagePlaceholderDrift() = %d, want %d", got, want)
}
if got := subtractMarkdownImagePlaceholderDrift(5, 4, images); got != 4 {
t.Fatalf("subtractMarkdownImagePlaceholderDrift() floor = %d, want 4", got)
}
}

func TestMarkdownImage_IsRemote(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading
Loading