Skip to content

Commit 368eb23

Browse files
committed
fix(docs): render markdown strikethrough
1 parent f0dbde2 commit 368eb23

5 files changed

Lines changed: 112 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
### Fixed
1212

13+
- Docs: render GFM `~~strikethrough~~` spans in the local markdown writer used by `docs write --tab --markdown`. (#702)
1314
- 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.
1415
- 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.
1516

internal/cmd/docs_formatter.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ func buildTextStyleRequest(style TextStyle, baseOffset int64, tabID string) *doc
333333
textStyle.Italic = true
334334
fields = append(fields, "italic")
335335
}
336+
if style.Strikethrough {
337+
textStyle.Strikethrough = true
338+
fields = append(fields, "strikethrough")
339+
}
336340
if style.Code {
337341
textStyle.WeightedFontFamily = &docs.WeightedFontFamily{
338342
FontFamily: "Courier New",

internal/cmd/docs_formatter_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,38 @@ func TestMarkdownToDocsRequests_TableStartIndexUsesBase(t *testing.T) {
4343
}
4444
}
4545

46+
func TestMarkdownToDocsRequests_Strikethrough(t *testing.T) {
47+
elements := []MarkdownElement{{Type: MDParagraph, Content: "~~struck out~~ vs **bold**"}}
48+
requests, text, tables := MarkdownToDocsRequests(elements, 10, "t.second")
49+
50+
if text != "struck out vs bold\n" {
51+
t.Fatalf("unexpected text: %q", text)
52+
}
53+
if len(tables) != 0 {
54+
t.Fatalf("unexpected tables: %d", len(tables))
55+
}
56+
57+
var sawStrike bool
58+
for _, req := range requests {
59+
if req.UpdateTextStyle == nil || req.UpdateTextStyle.TextStyle == nil {
60+
continue
61+
}
62+
if !req.UpdateTextStyle.TextStyle.Strikethrough {
63+
continue
64+
}
65+
sawStrike = true
66+
if req.UpdateTextStyle.Fields != "strikethrough" {
67+
t.Fatalf("unexpected strikethrough fields: %q", req.UpdateTextStyle.Fields)
68+
}
69+
if got := req.UpdateTextStyle.Range; got.StartIndex != 10 || got.EndIndex != 20 || got.TabId != "t.second" {
70+
t.Fatalf("unexpected strikethrough range: %#v", got)
71+
}
72+
}
73+
if !sawStrike {
74+
t.Fatalf("missing strikethrough request: %#v", requests)
75+
}
76+
}
77+
4678
// TestMarkdownToDocsRequests_AppendBulletsAndCode is a regression test for
4779
// #594. The append path used to inline literal "• " glyphs for bullet lists
4880
// (leaving paragraphs as NORMAL_TEXT) and split fenced code blocks into one

internal/cmd/docs_markdown.go

Lines changed: 40 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ type MarkdownElement struct {
4848

4949
// TextStyle represents text formatting
5050
type TextStyle struct {
51-
Bold bool
52-
Italic bool
53-
Code bool
54-
Link string
55-
Start int64
56-
End int64
51+
Bold bool
52+
Italic bool
53+
Strikethrough bool
54+
Code bool
55+
Link string
56+
Start int64
57+
End int64
5758
}
5859

5960
// ParagraphStyle represents paragraph-level formatting
@@ -423,17 +424,18 @@ func parseInlineSegment(text string) (string, []TextStyle) {
423424
}
424425
}
425426

426-
if marker, bold, italic, ok := inlineMarkerAt(text, i); ok {
427+
if marker, bold, italic, strikethrough, ok := inlineMarkerAt(text, i); ok {
427428
searchFrom := i + len(marker)
428429
if end := findClosingInlineMarker(text, searchFrom, marker); end >= 0 && end > searchFrom {
429430
content, nestedStyles := parseInlineSegment(text[searchFrom:end])
430431
start := utf16Len(stripped.String())
431432
stripped.WriteString(content)
432433
styles = append(styles, TextStyle{
433-
Start: start,
434-
End: start + utf16Len(content),
435-
Bold: bold,
436-
Italic: italic,
434+
Start: start,
435+
End: start + utf16Len(content),
436+
Bold: bold,
437+
Italic: italic,
438+
Strikethrough: strikethrough,
437439
})
438440
styles = appendShiftedStyles(styles, nestedStyles, start)
439441
i = end + len(marker)
@@ -501,24 +503,41 @@ func appendShiftedStyles(styles []TextStyle, nested []TextStyle, offset int64) [
501503
return styles
502504
}
503505

504-
func inlineMarkerAt(text string, i int) (marker string, bold bool, italic bool, ok bool) {
505-
for _, candidate := range []string{"***", "___", "**", "__", "*", "_"} {
506+
func inlineMarkerAt(text string, i int) (marker string, bold bool, italic bool, strikethrough bool, ok bool) {
507+
for _, candidate := range []string{"***", "___", "**", "__", "~~", "*", "_"} {
506508
if !strings.HasPrefix(text[i:], candidate) {
507509
continue
508510
}
509511
if candidate[0] == '_' && !isUnderscoreOpeningDelimiter(text, i, len(candidate)) {
510-
return "", false, false, false
512+
return "", false, false, false, false
513+
}
514+
if candidate == "~~" {
515+
if i > 0 && text[i-1] == '~' {
516+
return "", false, false, false, false
517+
}
518+
if tildeRunLenAt(text, i) != len(candidate) {
519+
return "", false, false, false, false
520+
}
521+
return candidate, false, false, true, true
511522
}
512523
switch len(candidate) {
513524
case 3:
514-
return candidate, true, true, true
525+
return candidate, true, true, false, true
515526
case 2:
516-
return candidate, true, false, true
527+
return candidate, true, false, false, true
517528
default:
518-
return candidate, false, true, true
529+
return candidate, false, true, false, true
519530
}
520531
}
521-
return "", false, false, false
532+
return "", false, false, false, false
533+
}
534+
535+
func tildeRunLenAt(text string, i int) int {
536+
runEnd := i
537+
for runEnd < len(text) && text[runEnd] == '~' {
538+
runEnd++
539+
}
540+
return runEnd - i
522541
}
523542

524543
func findClosingInlineMarker(text string, searchFrom int, marker string) int {
@@ -553,6 +572,9 @@ func closingInlineMarkerInRun(text string, searchFrom int, i int, marker string)
553572
if runLen < markerLen {
554573
return 0, runEnd, false
555574
}
575+
if marker == "~~" {
576+
return i, runEnd, runLen == markerLen
577+
}
556578

557579
if markerLen == 1 {
558580
if runLen == 1 {

internal/cmd/docs_markdown_test.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,27 @@ func TestParseInlineFormatting_NestedAndUnderscoreStyles(t *testing.T) {
146146
assertInlineStyle(t, text, styles, "both", true, true, false)
147147
}
148148

149+
func TestParseInlineFormatting_Strikethrough(t *testing.T) {
150+
styles, text := ParseInlineFormatting("~~struck out~~ vs **bold**")
151+
if text != "struck out vs bold" {
152+
t.Fatalf("text = %q", text)
153+
}
154+
155+
assertInlineStrikethrough(t, text, styles, "struck out")
156+
assertInlineStyle(t, text, styles, "bold", true, false, false)
157+
}
158+
159+
func TestParseInlineFormatting_LongTildeRunsAreLiteral(t *testing.T) {
160+
styles, text := ParseInlineFormatting("~~ok~~ and ~~~not~~~ and ~~~~also not~~~~")
161+
if text != "ok and ~~~not~~~ and ~~~~also not~~~~" {
162+
t.Fatalf("text = %q", text)
163+
}
164+
if len(styles) != 1 {
165+
t.Fatalf("expected only the exact two-tilde span to format, got %#v", styles)
166+
}
167+
assertInlineStrikethrough(t, text, styles, "ok")
168+
}
169+
149170
func TestParseInlineFormatting_ClosingMarkerIgnoresCodeSpan(t *testing.T) {
150171
styles, text := ParseInlineFormatting("**Use `**` marker** and _keep `_` literal_")
151172
if text != "Use ** marker and keep _ literal" {
@@ -259,13 +280,26 @@ func assertInlineStyle(t *testing.T, text string, styles []TextStyle, wantText s
259280
if int(style.End) > len(text) {
260281
continue
261282
}
262-
if text[style.Start:style.End] == wantText && style.Bold == bold && style.Italic == italic && style.Code == code {
283+
if text[style.Start:style.End] == wantText && style.Bold == bold && style.Italic == italic && style.Code == code && !style.Strikethrough {
263284
return
264285
}
265286
}
266287
t.Fatalf("missing style text=%q bold=%v italic=%v code=%v in %#v", wantText, bold, italic, code, styles)
267288
}
268289

290+
func assertInlineStrikethrough(t *testing.T, text string, styles []TextStyle, wantText string) {
291+
t.Helper()
292+
for _, style := range styles {
293+
if int(style.End) > len(text) {
294+
continue
295+
}
296+
if text[style.Start:style.End] == wantText && style.Strikethrough {
297+
return
298+
}
299+
}
300+
t.Fatalf("missing strikethrough text=%q in %#v", wantText, styles)
301+
}
302+
269303
func assertInlineLink(t *testing.T, text string, styles []TextStyle, wantText string, wantURL string) {
270304
t.Helper()
271305
for _, style := range styles {

0 commit comments

Comments
 (0)