Skip to content
Open
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 @@ -11,6 +11,7 @@

- CLI: harden backup writes, config/credentials atomic saves, keyring write verification, line input buffering, disabled-API hints, JSON transform number handling, and untrusted-content wrapping after ClawPatch review.
- CLI: bound retry request replay buffering, recover failed async backup pushes, ignore global git commit signing in backup snapshots, and protect account manager OAuth redirects with CSRF checks.
- Docs: `docs write --append --markdown` now expands inline markdown markers (`**bold**`, `*italic*`, `` `code` ``, `[link](url)`) inside table cells into character runs, matching the behaviour outside of tables — previously the markers rendered as literal characters because the table inserter bypassed the inline-formatting pass. (#608)

## 0.17.0 - 2026-05-15

Expand Down
78 changes: 47 additions & 31 deletions internal/cmd/docs_table_inserter.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,36 +74,9 @@ func (ti *TableInserter) InsertNativeTable(ctx context.Context, tableIndex int64
continue
}

// Insert text into cell
insertTextReq := &docs.Request{
InsertText: &docs.InsertTextRequest{
Location: &docs.Location{
Index: cellIdx,
},
Text: cellContent,
},
}

// Make text bold if it's a header row
var boldReq *docs.Request
if rowIdx == 0 {
boldReq = &docs.Request{
UpdateTextStyle: &docs.UpdateTextStyleRequest{
Range: &docs.Range{
StartIndex: cellIdx,
EndIndex: cellIdx + utf16Len(cellContent),
},
TextStyle: &docs.TextStyle{
Bold: true,
},
Fields: "bold",
},
}
}

requests := []*docs.Request{insertTextReq}
if boldReq != nil {
requests = append(requests, boldReq)
requests, insertedLen := buildTableCellRequests(cellContent, cellIdx, rowIdx == 0)
if len(requests) == 0 {
continue
}

_, err := ti.svc.Documents.BatchUpdate(ti.docID, &docs.BatchUpdateDocumentRequest{
Expand All @@ -114,13 +87,56 @@ func (ti *TableInserter) InsertNativeTable(ctx context.Context, tableIndex int64
}

// Update indices for subsequent cells (they shift by the content length)
ti.updateIndicesAfter(cellIdx, utf16Len(cellContent), cellIndices, &tableEndIndex)
ti.updateIndicesAfter(cellIdx, insertedLen, cellIndices, &tableEndIndex)
}
}

return tableEndIndex, nil
}

// buildTableCellRequests constructs the batch requests required to populate a
// single table cell, expanding inline markdown (**bold**, *italic*, `code`,
// [links]) into UpdateTextStyle requests on top of the inserted text. Header
// cells additionally receive a whole-cell bold style. Returns the requests and
// the UTF-16 length of the text that will be inserted so callers can keep
// running cell indices in sync. If the cell content strips to an empty string
// (e.g. content was only markers), returns (nil, 0).
func buildTableCellRequests(cellContent string, cellIdx int64, isHeaderRow bool) ([]*docs.Request, int64) {
styles, stripped := ParseInlineFormatting(cellContent)
if stripped == "" {
return nil, 0
}

insertedLen := utf16Len(stripped)
requests := []*docs.Request{{
InsertText: &docs.InsertTextRequest{
Location: &docs.Location{Index: cellIdx},
Text: stripped,
},
}}

if isHeaderRow {
requests = append(requests, &docs.Request{
UpdateTextStyle: &docs.UpdateTextStyleRequest{
Range: &docs.Range{
StartIndex: cellIdx,
EndIndex: cellIdx + insertedLen,
},
TextStyle: &docs.TextStyle{Bold: true},
Fields: "bold",
},
})
}

for _, style := range styles {
if req := buildTextStyleRequest(style, cellIdx, ""); req != nil {
requests = append(requests, req)
}
}

return requests, insertedLen
}

// getTableCellIndices extracts the start index for each cell in a table
func (ti *TableInserter) getTableCellIndices(doc *docs.Document, tableStartIndex int64, rows, cols int64) ([][]int64, int64, error) {
cellIndices := make([][]int64, rows)
Expand Down
124 changes: 124 additions & 0 deletions internal/cmd/docs_table_inserter_inline_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package cmd

import (
"testing"

"google.golang.org/api/docs/v1"
)

// Regression for #608: inline markdown markers inside table cells previously
// rendered as literal characters because the inserter passed the cell content
// straight through to InsertText without running the same inline-formatting
// pass used by paragraphs/headings.

func TestBuildTableCellRequests_AppliesInlineBold(t *testing.T) {
reqs, inserted := buildTableCellRequests("**Alice**", 100, false)

if inserted != utf16Len("Alice") {
t.Fatalf("expected inserted len = utf16Len(\"Alice\") = %d, got %d", utf16Len("Alice"), inserted)
}

if len(reqs) != 2 {
t.Fatalf("expected 2 requests (InsertText + UpdateTextStyle), got %d: %#v", len(reqs), reqs)
}

insert := reqs[0].InsertText
if insert == nil || insert.Text != "Alice" {
t.Fatalf("expected InsertText with stripped text \"Alice\", got %#v", reqs[0])
}
if insert.Location == nil || insert.Location.Index != 100 {
t.Fatalf("expected insert at index 100, got %#v", insert.Location)
}

style := reqs[1].UpdateTextStyle
if style == nil || style.TextStyle == nil || !style.TextStyle.Bold {
t.Fatalf("expected UpdateTextStyle with Bold:true, got %#v", reqs[1])
}
if style.Range == nil || style.Range.StartIndex != 100 || style.Range.EndIndex != 100+utf16Len("Alice") {
t.Fatalf("expected style range [100,105], got %#v", style.Range)
}
}

func TestBuildTableCellRequests_AppliesInlineItalicAndCode(t *testing.T) {
cases := []struct {
name string
cell string
wantText string
wantBold bool
wantItalic bool
wantCode bool
}{
{"italic", "*important*", "important", false, true, false},
{"code", "`xyz123`", "xyz123", false, false, true},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
reqs, inserted := buildTableCellRequests(tt.cell, 50, false)
if inserted != utf16Len(tt.wantText) {
t.Fatalf("inserted = %d, want %d", inserted, utf16Len(tt.wantText))
}
if len(reqs) < 2 {
t.Fatalf("expected >=2 requests, got %d: %#v", len(reqs), reqs)
}
if got := reqs[0].InsertText; got == nil || got.Text != tt.wantText {
t.Fatalf("InsertText.Text = %q, want %q", textOf(reqs[0]), tt.wantText)
}
style := reqs[1].UpdateTextStyle
if style == nil || style.TextStyle == nil {
t.Fatalf("expected UpdateTextStyle, got %#v", reqs[1])
}
if tt.wantItalic && !style.TextStyle.Italic {
t.Fatalf("expected Italic, got %#v", style.TextStyle)
}
if tt.wantCode && style.TextStyle.WeightedFontFamily == nil {
t.Fatalf("expected code styling (WeightedFontFamily), got %#v", style.TextStyle)
}
})
}
}

func TestBuildTableCellRequests_HeaderRowAppliesBoldOverWholeCell(t *testing.T) {
reqs, inserted := buildTableCellRequests("Field", 10, true)

if inserted != utf16Len("Field") {
t.Fatalf("inserted = %d, want %d", inserted, utf16Len("Field"))
}
if len(reqs) != 2 {
t.Fatalf("expected 2 requests (InsertText + header bold), got %d: %#v", len(reqs), reqs)
}
style := reqs[1].UpdateTextStyle
if style == nil || style.TextStyle == nil || !style.TextStyle.Bold {
t.Fatalf("expected header-row UpdateTextStyle with Bold:true, got %#v", reqs[1])
}
if style.Range == nil || style.Range.StartIndex != 10 || style.Range.EndIndex != 10+utf16Len("Field") {
t.Fatalf("expected style range [10,15], got %#v", style.Range)
}
}

func TestBuildTableCellRequests_PlainTextNoStyleRequest(t *testing.T) {
reqs, inserted := buildTableCellRequests("plain text", 1, false)
if inserted != utf16Len("plain text") {
t.Fatalf("inserted = %d, want %d", inserted, utf16Len("plain text"))
}
if len(reqs) != 1 {
t.Fatalf("expected just InsertText, got %d: %#v", len(reqs), reqs)
}
if got := reqs[0].InsertText; got == nil || got.Text != "plain text" {
t.Fatalf("InsertText.Text = %q, want %q", textOf(reqs[0]), "plain text")
}
}

func TestBuildTableCellRequests_EmptyAfterStrippingReturnsNothing(t *testing.T) {
// A cell whose entire content is markers (e.g. "**") would strip to "".
reqs, inserted := buildTableCellRequests("", 1, false)
if len(reqs) != 0 || inserted != 0 {
t.Fatalf("expected (nil, 0) for empty cell, got reqs=%#v inserted=%d", reqs, inserted)
}
}

func textOf(r *docs.Request) string {
if r == nil || r.InsertText == nil {
return ""
}
return r.InsertText.Text
}