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 @@ -10,6 +10,7 @@

### Fixed

- 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
4 changes: 4 additions & 0 deletions internal/cmd/docs_formatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,10 @@ func buildTextStyleRequest(style TextStyle, baseOffset int64, tabID string) *doc
textStyle.Italic = true
fields = append(fields, "italic")
}
if style.Strikethrough {
textStyle.Strikethrough = true
fields = append(fields, "strikethrough")
}
if style.Code {
textStyle.WeightedFontFamily = &docs.WeightedFontFamily{
FontFamily: "Courier New",
Expand Down
32 changes: 32 additions & 0 deletions internal/cmd/docs_formatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,38 @@ func TestMarkdownToDocsRequests_TableStartIndexUsesBase(t *testing.T) {
}
}

func TestMarkdownToDocsRequests_Strikethrough(t *testing.T) {
elements := []MarkdownElement{{Type: MDParagraph, Content: "~~struck out~~ vs **bold**"}}
requests, text, tables := MarkdownToDocsRequests(elements, 10, "t.second")

if text != "struck out vs bold\n" {
t.Fatalf("unexpected text: %q", text)
}
if len(tables) != 0 {
t.Fatalf("unexpected tables: %d", len(tables))
}

var sawStrike bool
for _, req := range requests {
if req.UpdateTextStyle == nil || req.UpdateTextStyle.TextStyle == nil {
continue
}
if !req.UpdateTextStyle.TextStyle.Strikethrough {
continue
}
sawStrike = true
if req.UpdateTextStyle.Fields != "strikethrough" {
t.Fatalf("unexpected strikethrough fields: %q", req.UpdateTextStyle.Fields)
}
if got := req.UpdateTextStyle.Range; got.StartIndex != 10 || got.EndIndex != 20 || got.TabId != "t.second" {
t.Fatalf("unexpected strikethrough range: %#v", got)
}
}
if !sawStrike {
t.Fatalf("missing strikethrough request: %#v", requests)
}
}

// TestMarkdownToDocsRequests_AppendBulletsAndCode is a regression test for
// #594. The append path used to inline literal "• " glyphs for bullet lists
// (leaving paragraphs as NORMAL_TEXT) and split fenced code blocks into one
Expand Down
58 changes: 40 additions & 18 deletions internal/cmd/docs_markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,13 @@ type MarkdownElement struct {

// TextStyle represents text formatting
type TextStyle struct {
Bold bool
Italic bool
Code bool
Link string
Start int64
End int64
Bold bool
Italic bool
Strikethrough bool
Code bool
Link string
Start int64
End int64
}

// ParagraphStyle represents paragraph-level formatting
Expand Down Expand Up @@ -423,17 +424,18 @@ func parseInlineSegment(text string) (string, []TextStyle) {
}
}

if marker, bold, italic, ok := inlineMarkerAt(text, i); ok {
if marker, bold, italic, strikethrough, ok := inlineMarkerAt(text, i); ok {
searchFrom := i + len(marker)
if end := findClosingInlineMarker(text, searchFrom, marker); end >= 0 && end > searchFrom {
content, nestedStyles := parseInlineSegment(text[searchFrom:end])
start := utf16Len(stripped.String())
stripped.WriteString(content)
styles = append(styles, TextStyle{
Start: start,
End: start + utf16Len(content),
Bold: bold,
Italic: italic,
Start: start,
End: start + utf16Len(content),
Bold: bold,
Italic: italic,
Strikethrough: strikethrough,
})
styles = appendShiftedStyles(styles, nestedStyles, start)
i = end + len(marker)
Expand Down Expand Up @@ -501,24 +503,41 @@ func appendShiftedStyles(styles []TextStyle, nested []TextStyle, offset int64) [
return styles
}

func inlineMarkerAt(text string, i int) (marker string, bold bool, italic bool, ok bool) {
for _, candidate := range []string{"***", "___", "**", "__", "*", "_"} {
func inlineMarkerAt(text string, i int) (marker string, bold bool, italic bool, strikethrough bool, ok bool) {
for _, candidate := range []string{"***", "___", "**", "__", "~~", "*", "_"} {
if !strings.HasPrefix(text[i:], candidate) {
continue
}
if candidate[0] == '_' && !isUnderscoreOpeningDelimiter(text, i, len(candidate)) {
return "", false, false, false
return "", false, false, false, false
}
if candidate == "~~" {
if i > 0 && text[i-1] == '~' {
return "", false, false, false, false
}
if tildeRunLenAt(text, i) != len(candidate) {
return "", false, false, false, false
}
return candidate, false, false, true, true
}
switch len(candidate) {
case 3:
return candidate, true, true, true
return candidate, true, true, false, true
case 2:
return candidate, true, false, true
return candidate, true, false, false, true
default:
return candidate, false, true, true
return candidate, false, true, false, true
}
}
return "", false, false, false
return "", false, false, false, false
}

func tildeRunLenAt(text string, i int) int {
runEnd := i
for runEnd < len(text) && text[runEnd] == '~' {
runEnd++
}
return runEnd - i
}

func findClosingInlineMarker(text string, searchFrom int, marker string) int {
Expand Down Expand Up @@ -553,6 +572,9 @@ func closingInlineMarkerInRun(text string, searchFrom int, i int, marker string)
if runLen < markerLen {
return 0, runEnd, false
}
if marker == "~~" {
return i, runEnd, runLen == markerLen
}

if markerLen == 1 {
if runLen == 1 {
Expand Down
36 changes: 35 additions & 1 deletion internal/cmd/docs_markdown_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ func TestParseInlineFormatting_NestedAndUnderscoreStyles(t *testing.T) {
assertInlineStyle(t, text, styles, "both", true, true, false)
}

func TestParseInlineFormatting_Strikethrough(t *testing.T) {
styles, text := ParseInlineFormatting("~~struck out~~ vs **bold**")
if text != "struck out vs bold" {
t.Fatalf("text = %q", text)
}

assertInlineStrikethrough(t, text, styles, "struck out")
assertInlineStyle(t, text, styles, "bold", true, false, false)
}

func TestParseInlineFormatting_LongTildeRunsAreLiteral(t *testing.T) {
styles, text := ParseInlineFormatting("~~ok~~ and ~~~not~~~ and ~~~~also not~~~~")
if text != "ok and ~~~not~~~ and ~~~~also not~~~~" {
t.Fatalf("text = %q", text)
}
if len(styles) != 1 {
t.Fatalf("expected only the exact two-tilde span to format, got %#v", styles)
}
assertInlineStrikethrough(t, text, styles, "ok")
}

func TestParseInlineFormatting_ClosingMarkerIgnoresCodeSpan(t *testing.T) {
styles, text := ParseInlineFormatting("**Use `**` marker** and _keep `_` literal_")
if text != "Use ** marker and keep _ literal" {
Expand Down Expand Up @@ -259,13 +280,26 @@ func assertInlineStyle(t *testing.T, text string, styles []TextStyle, wantText s
if int(style.End) > len(text) {
continue
}
if text[style.Start:style.End] == wantText && style.Bold == bold && style.Italic == italic && style.Code == code {
if text[style.Start:style.End] == wantText && style.Bold == bold && style.Italic == italic && style.Code == code && !style.Strikethrough {
return
}
}
t.Fatalf("missing style text=%q bold=%v italic=%v code=%v in %#v", wantText, bold, italic, code, styles)
}

func assertInlineStrikethrough(t *testing.T, text string, styles []TextStyle, wantText string) {
t.Helper()
for _, style := range styles {
if int(style.End) > len(text) {
continue
}
if text[style.Start:style.End] == wantText && style.Strikethrough {
return
}
}
t.Fatalf("missing strikethrough text=%q in %#v", wantText, styles)
}

func assertInlineLink(t *testing.T, text string, styles []TextStyle, wantText string, wantURL string) {
t.Helper()
for _, style := range styles {
Expand Down
Loading