From 32353e9c44a54f79ec1fa38d4d1d115bfdad3a69 Mon Sep 17 00:00:00 2001 From: Chris Hall Date: Tue, 19 May 2026 20:17:20 -0400 Subject: [PATCH 1/3] feat(docs): expose heading styles via docs format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #605. `docs format` covered text attributes and a couple of paragraph attributes (alignment, line spacing) but had no way to set a paragraph's named style — the Docs API field that drives outline view / navigation pane and is the difference between a flat doc and a structured one. The markdown writer can produce HEADING_N on fresh content via leading `#`s, but offered no way to restyle existing paragraphs. Adds two new DocsFormatFlags: - `--heading-level N` (1..6): shortcut for HEADING_N. - `--named-style NAME`: full enum — NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6 (case-insensitive). The two are mutually exclusive. Either sets paragraphStyle.namedStyleType on the existing UpdateParagraphStyle request, so the new flags compose cleanly with --alignment and --line-spacing without an extra round-trip. Helper docsFormatNamedStyle does the validation + enum resolution. Tests cover all six heading levels, the full enum, case-insensitivity, mutual-exclusion, range-rejection, unknown-enum rejection, composition with alignment, and the any() detection used by the no-flags guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/cmd/docs_format.go | 44 +++++++- internal/cmd/docs_format_headings_test.go | 124 ++++++++++++++++++++++ 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 internal/cmd/docs_format_headings_test.go diff --git a/internal/cmd/docs_format.go b/internal/cmd/docs_format.go index e8b8f249..df12e785 100644 --- a/internal/cmd/docs_format.go +++ b/internal/cmd/docs_format.go @@ -37,6 +37,8 @@ type DocsFormatFlags struct { NoStrike bool `name:"no-strikethrough" aliases:"no-strike" help:"Clear strikethrough"` Alignment string `name:"alignment" help:"Paragraph alignment: left, center, right, justify, start, end, justified"` LineSpacing float64 `name:"line-spacing" help:"Paragraph line spacing percentage, for example 100 or 150"` + HeadingLevel int `name:"heading-level" help:"Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N)"` + NamedStyle string `name:"named-style" help:"Set paragraph named style: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6"` } func (c *DocsFormatCmd) Run(ctx context.Context, flags *RootFlags) error { @@ -82,6 +84,8 @@ func (c *DocsFormatCmd) Run(ctx context.Context, flags *RootFlags) error { "no_strike": c.Format.NoStrike, "alignment": c.Format.Alignment, "line_spacing": c.Format.LineSpacing, + "heading_level": c.Format.HeadingLevel, + "named_style": c.Format.NamedStyle, }, }); err != nil { return err @@ -207,7 +211,9 @@ func (f DocsFormatFlags) any() bool { f.Underline || f.NoUnderline || f.Strikethrough || f.NoStrike || strings.TrimSpace(f.Alignment) != "" || - f.LineSpacing != 0 + f.LineSpacing != 0 || + f.HeadingLevel != 0 || + strings.TrimSpace(f.NamedStyle) != "" } func (f DocsFormatFlags) buildRequests(start, end int64, tabID string) ([]*docs.Request, error) { @@ -321,6 +327,14 @@ func (f DocsFormatFlags) buildParagraphStyleRequest(start, end int64, tabID stri style.LineSpacing = f.LineSpacing fields = append(fields, "lineSpacing") } + namedStyle, err := docsFormatNamedStyle(f.HeadingLevel, f.NamedStyle) + if err != nil { + return nil, false, err + } + if namedStyle != "" { + style.NamedStyleType = namedStyle + fields = append(fields, "namedStyleType") + } if len(fields) == 0 { return nil, false, nil } @@ -331,6 +345,34 @@ func (f DocsFormatFlags) buildParagraphStyleRequest(start, end int64, tabID stri }}, true, nil } +// docsFormatNamedStyle resolves the named paragraph style requested via +// --heading-level (1..6 shortcut) and/or --named-style (full enum). The two +// are mutually exclusive — supplying both is a usage error. Returns "" when +// neither was supplied. +func docsFormatNamedStyle(headingLevel int, namedStyle string) (string, error) { + trimmed := strings.ToUpper(strings.TrimSpace(namedStyle)) + if headingLevel != 0 && trimmed != "" { + return "", usage("--heading-level and --named-style cannot be combined") + } + if headingLevel != 0 { + if headingLevel < 1 || headingLevel > 6 { + return "", usage("--heading-level must be between 1 and 6") + } + return fmt.Sprintf("HEADING_%d", headingLevel), nil + } + if trimmed == "" { + return "", nil + } + switch trimmed { + case "NORMAL_TEXT", "TITLE", "SUBTITLE", + "HEADING_1", "HEADING_2", "HEADING_3", + "HEADING_4", "HEADING_5", "HEADING_6": + return trimmed, nil + default: + return "", usage("--named-style must be one of NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6") + } +} + func docsFormatColor(hex, flag string) (*docs.OptionalColor, error) { r, g, b, ok := parseHexColor(hex) if !ok { diff --git a/internal/cmd/docs_format_headings_test.go b/internal/cmd/docs_format_headings_test.go new file mode 100644 index 00000000..11c3e2a6 --- /dev/null +++ b/internal/cmd/docs_format_headings_test.go @@ -0,0 +1,124 @@ +package cmd + +import ( + "strings" + "testing" +) + +// Coverage for #605: --heading-level (1-6 shortcut) and --named-style flags +// surface the Docs API paragraphStyle.namedStyleType field on `docs format` +// so existing paragraphs can be restyled into HEADING_1..HEADING_6, TITLE, +// SUBTITLE, or NORMAL_TEXT without rewriting them through the markdown path. + +func TestDocsFormatFlags_HeadingLevelEmitsNamedStyleType(t *testing.T) { + cases := []struct { + name string + level int + want string + }{ + {"H1", 1, "HEADING_1"}, + {"H2", 2, "HEADING_2"}, + {"H6", 6, "HEADING_6"}, + } + for _, tt := range cases { + t.Run(tt.name, func(t *testing.T) { + reqs, err := (DocsFormatFlags{HeadingLevel: tt.level}).buildRequests(3, 9, "") + if err != nil { + t.Fatalf("buildRequests: %v", err) + } + if len(reqs) != 1 || reqs[0].UpdateParagraphStyle == nil { + t.Fatalf("expected one paragraph-style request, got %#v", reqs) + } + pr := reqs[0].UpdateParagraphStyle + if pr.ParagraphStyle.NamedStyleType != tt.want { + t.Fatalf("NamedStyleType = %q, want %q", pr.ParagraphStyle.NamedStyleType, tt.want) + } + if !strings.Contains(pr.Fields, "namedStyleType") { + t.Fatalf("Fields must include namedStyleType, got %q", pr.Fields) + } + }) + } +} + +func TestDocsFormatFlags_NamedStyleAcceptsAllValidEnums(t *testing.T) { + for _, ns := range []string{"NORMAL_TEXT", "TITLE", "SUBTITLE", "HEADING_1", "HEADING_2", "HEADING_3", "HEADING_4", "HEADING_5", "HEADING_6"} { + t.Run(ns, func(t *testing.T) { + reqs, err := (DocsFormatFlags{NamedStyle: ns}).buildRequests(1, 2, "") + if err != nil { + t.Fatalf("buildRequests(%q): %v", ns, err) + } + if got := reqs[0].UpdateParagraphStyle.ParagraphStyle.NamedStyleType; got != ns { + t.Fatalf("NamedStyleType = %q, want %q", got, ns) + } + }) + } +} + +func TestDocsFormatFlags_NamedStyleIsCaseInsensitive(t *testing.T) { + reqs, err := (DocsFormatFlags{NamedStyle: "heading_3"}).buildRequests(1, 2, "") + if err != nil { + t.Fatalf("buildRequests: %v", err) + } + if got := reqs[0].UpdateParagraphStyle.ParagraphStyle.NamedStyleType; got != "HEADING_3" { + t.Fatalf("NamedStyleType = %q, want HEADING_3", got) + } +} + +func TestDocsFormatFlags_HeadingLevelAndNamedStyleMutuallyExclusive(t *testing.T) { + _, err := (DocsFormatFlags{HeadingLevel: 1, NamedStyle: "TITLE"}).buildRequests(1, 2, "") + if err == nil || !strings.Contains(err.Error(), "cannot be combined") { + t.Fatalf("expected mutual-exclusion error, got %v", err) + } +} + +func TestDocsFormatFlags_HeadingLevelOutOfRangeRejected(t *testing.T) { + for _, lvl := range []int{-1, 0, 7, 99} { + if lvl == 0 { + // 0 means "no heading level" — not an error on its own. + continue + } + _, err := (DocsFormatFlags{HeadingLevel: lvl}).buildRequests(1, 2, "") + if err == nil || !strings.Contains(err.Error(), "--heading-level must be between 1 and 6") { + t.Fatalf("level %d: expected range error, got %v", lvl, err) + } + } +} + +func TestDocsFormatFlags_UnknownNamedStyleRejected(t *testing.T) { + _, err := (DocsFormatFlags{NamedStyle: "BANNER"}).buildRequests(1, 2, "") + if err == nil || !strings.Contains(err.Error(), "--named-style must be one of") { + t.Fatalf("expected named-style enum error, got %v", err) + } +} + +func TestDocsFormatFlags_HeadingComposesWithAlignment(t *testing.T) { + reqs, err := (DocsFormatFlags{HeadingLevel: 1, Alignment: "center"}).buildRequests(3, 9, "t.tab") + if err != nil { + t.Fatalf("buildRequests: %v", err) + } + if len(reqs) != 1 { + t.Fatalf("expected one paragraph request combining alignment + heading, got %d", len(reqs)) + } + pr := reqs[0].UpdateParagraphStyle + if pr.ParagraphStyle.NamedStyleType != "HEADING_1" || pr.ParagraphStyle.Alignment != "CENTER" { + t.Fatalf("unexpected style: %#v", pr.ParagraphStyle) + } + if !strings.Contains(pr.Fields, "namedStyleType") || !strings.Contains(pr.Fields, "alignment") { + t.Fatalf("Fields missing combined attrs: %q", pr.Fields) + } + if pr.Range.TabId != "t.tab" { + t.Fatalf("range lost tab: %#v", pr.Range) + } +} + +func TestDocsFormatFlags_AnyDetectsHeadingFlags(t *testing.T) { + if !(DocsFormatFlags{HeadingLevel: 2}).any() { + t.Fatalf("any() should be true when HeadingLevel is set") + } + if !(DocsFormatFlags{NamedStyle: "TITLE"}).any() { + t.Fatalf("any() should be true when NamedStyle is set") + } + if (DocsFormatFlags{}).any() { + t.Fatalf("any() should be false when nothing is set") + } +} From 1d920da4ab8183972d297f655906fcbe535cf455 Mon Sep 17 00:00:00 2001 From: Chris Hall Date: Tue, 19 May 2026 20:40:12 -0400 Subject: [PATCH 2/3] docs: changelog + regen for #605 heading styles in docs format Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 1 + docs/commands/gog-docs-format.md | 2 ++ docs/commands/gog-docs-write.md | 2 ++ docs/docs-editing.md | 11 +++++++++++ 4 files changed, 16 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 808afe9b..d91e77d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Calendar: add --with-zoom / --regenerate-zoom / --remove-zoom that create, regenerate, and remove Zoom meetings and attach the join URL + meeting ID + passcode to the Calendar event description. Google's Calendar API rejects conferenceData writes asserting `conferenceSolution.key.type="addOn"` from non-Workspace-Marketplace OAuth clients, so the description-mode integration is the path that round-trips through Google's storage; trade-off is no native "Join with Zoom" conference card. (#589, #590) — thanks @alexisperumal and @mvanhorn. - Auth: add gog zoom auth setup / doctor for Zoom S2S OAuth credential storage. (#590) — thanks @mvanhorn. - Docs: add `gog docs insert-page-break [--index N | --at-end] [--tab=STRING]` to insert a Google Docs page break directly via `InsertPageBreakRequest` — markdown has no native page-break construct, so this is the only path for multi-page deliverables. Aliases: `page-break`, `pb`. (#604) +- Docs: add `--heading-level N` (1..6 shortcut) and `--named-style NAME` (full enum) to `gog docs format` so existing paragraphs can be promoted to `HEADING_1`..`HEADING_6`, `TITLE`, `SUBTITLE`, or `NORMAL_TEXT`. Both set `paragraphStyle.namedStyleType` on the existing UpdateParagraphStyle request and compose cleanly with `--alignment` / `--line-spacing`. (#605) ### Fixed diff --git a/docs/commands/gog-docs-format.md b/docs/commands/gog-docs-format.md index 910a63f7..306f36e6 100644 --- a/docs/commands/gog-docs-format.md +++ b/docs/commands/gog-docs-format.md @@ -32,6 +32,7 @@ gog docs (doc) format [flags] | `--font-size` | `float64` | | Font size in points | | `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands | | `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) | +| `--heading-level` | `int` | | Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N) | | `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. | | `--italic` | `bool` | | Set italic | | `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | @@ -39,6 +40,7 @@ gog docs (doc) format [flags] | `--match` | `string` | | Only format the first text match | | `--match-all` | `bool` | | Format all matches instead of only the first | | `--match-case` | `bool` | | Use case-sensitive matching with --match | +| `--named-style` | `string` | | Set paragraph named style: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6 | | `--no-bold` | `bool` | | Clear bold | | `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) | | `--no-italic` | `bool` | | Clear italic | diff --git a/docs/commands/gog-docs-write.md b/docs/commands/gog-docs-write.md index c79d83ad..b3cad78b 100644 --- a/docs/commands/gog-docs-write.md +++ b/docs/commands/gog-docs-write.md @@ -34,11 +34,13 @@ gog docs (doc) write [flags] | `--font-size` | `float64` | | Font size in points | | `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands | | `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) | +| `--heading-level` | `int` | | Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N) | | `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. | | `--italic` | `bool` | | Set italic | | `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | | `--line-spacing` | `float64` | | Paragraph line spacing percentage, for example 100 or 150 | | `--markdown` | `bool` | | Convert markdown to Google Docs formatting (requires --replace or --append) | +| `--named-style` | `string` | | Set paragraph named style: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6 | | `--no-bold` | `bool` | | Clear bold | | `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) | | `--no-italic` | `bool` | | Clear italic | diff --git a/docs/docs-editing.md b/docs/docs-editing.md index 4d2cbdb8..ffa7913d 100644 --- a/docs/docs-editing.md +++ b/docs/docs-editing.md @@ -37,6 +37,17 @@ gog docs format --match "Action item" --text-color '#b00020' gog docs format --match Heading --alignment center --line-spacing 120 ``` +Promote an existing paragraph to a heading or title style with +`--heading-level N` (1..6 shortcut) or `--named-style NAME` (full enum: +`NORMAL_TEXT`, `TITLE`, `SUBTITLE`, `HEADING_1`..`HEADING_6`, +case-insensitive). Both set `paragraphStyle.namedStyleType` on the same +update so they compose with `--alignment` and `--line-spacing`: + +```bash +gog docs format --match "Status" --heading-level 2 +gog docs format --match "Overview" --named-style title --alignment center +``` + Use `--match-all` when every occurrence should be formatted. Command page: From 476218570fcc46601c5550bc64a6f0519dc1ba65 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 20 May 2026 21:37:50 +0100 Subject: [PATCH 3/3] fix(docs): reject zero heading format level --- docs/commands/gog-docs-format.md | 2 +- docs/commands/gog-docs-write.md | 2 +- internal/cmd/docs_format.go | 32 +++++++++++------ internal/cmd/docs_format_headings_test.go | 42 ++++++++++++++--------- 4 files changed, 49 insertions(+), 29 deletions(-) diff --git a/docs/commands/gog-docs-format.md b/docs/commands/gog-docs-format.md index 306f36e6..56767793 100644 --- a/docs/commands/gog-docs-format.md +++ b/docs/commands/gog-docs-format.md @@ -32,7 +32,7 @@ gog docs (doc) format [flags] | `--font-size` | `float64` | | Font size in points | | `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands | | `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) | -| `--heading-level` | `int` | | Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N) | +| `--heading-level` | `*int` | | Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N) | | `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. | | `--italic` | `bool` | | Set italic | | `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | diff --git a/docs/commands/gog-docs-write.md b/docs/commands/gog-docs-write.md index b3cad78b..199f8688 100644 --- a/docs/commands/gog-docs-write.md +++ b/docs/commands/gog-docs-write.md @@ -34,7 +34,7 @@ gog docs (doc) write [flags] | `--font-size` | `float64` | | Font size in points | | `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands | | `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) | -| `--heading-level` | `int` | | Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N) | +| `--heading-level` | `*int` | | Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N) | | `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. | | `--italic` | `bool` | | Set italic | | `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) | diff --git a/internal/cmd/docs_format.go b/internal/cmd/docs_format.go index df12e785..0d350cf6 100644 --- a/internal/cmd/docs_format.go +++ b/internal/cmd/docs_format.go @@ -37,10 +37,22 @@ type DocsFormatFlags struct { NoStrike bool `name:"no-strikethrough" aliases:"no-strike" help:"Clear strikethrough"` Alignment string `name:"alignment" help:"Paragraph alignment: left, center, right, justify, start, end, justified"` LineSpacing float64 `name:"line-spacing" help:"Paragraph line spacing percentage, for example 100 or 150"` - HeadingLevel int `name:"heading-level" help:"Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N)"` + HeadingLevel *int `name:"heading-level" help:"Set paragraph named style to HEADING_1..HEADING_6 (shortcut for --named-style=HEADING_N)"` NamedStyle string `name:"named-style" help:"Set paragraph named style: NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6"` } +const ( + docsNamedStyleNormalText = "NORMAL_TEXT" + docsNamedStyleTitle = "TITLE" + docsNamedStyleSubtitle = "SUBTITLE" + docsNamedStyleHeading1 = "HEADING_1" + docsNamedStyleHeading2 = "HEADING_2" + docsNamedStyleHeading3 = "HEADING_3" + docsNamedStyleHeading4 = "HEADING_4" + docsNamedStyleHeading5 = "HEADING_5" + docsNamedStyleHeading6 = "HEADING_6" +) + func (c *DocsFormatCmd) Run(ctx context.Context, flags *RootFlags) error { id := strings.TrimSpace(c.DocID) if id == "" { @@ -212,7 +224,7 @@ func (f DocsFormatFlags) any() bool { f.Strikethrough || f.NoStrike || strings.TrimSpace(f.Alignment) != "" || f.LineSpacing != 0 || - f.HeadingLevel != 0 || + f.HeadingLevel != nil || strings.TrimSpace(f.NamedStyle) != "" } @@ -349,24 +361,24 @@ func (f DocsFormatFlags) buildParagraphStyleRequest(start, end int64, tabID stri // --heading-level (1..6 shortcut) and/or --named-style (full enum). The two // are mutually exclusive — supplying both is a usage error. Returns "" when // neither was supplied. -func docsFormatNamedStyle(headingLevel int, namedStyle string) (string, error) { +func docsFormatNamedStyle(headingLevel *int, namedStyle string) (string, error) { trimmed := strings.ToUpper(strings.TrimSpace(namedStyle)) - if headingLevel != 0 && trimmed != "" { + if headingLevel != nil && trimmed != "" { return "", usage("--heading-level and --named-style cannot be combined") } - if headingLevel != 0 { - if headingLevel < 1 || headingLevel > 6 { + if headingLevel != nil { + if *headingLevel < 1 || *headingLevel > 6 { return "", usage("--heading-level must be between 1 and 6") } - return fmt.Sprintf("HEADING_%d", headingLevel), nil + return fmt.Sprintf("HEADING_%d", *headingLevel), nil } if trimmed == "" { return "", nil } switch trimmed { - case "NORMAL_TEXT", "TITLE", "SUBTITLE", - "HEADING_1", "HEADING_2", "HEADING_3", - "HEADING_4", "HEADING_5", "HEADING_6": + case docsNamedStyleNormalText, docsNamedStyleTitle, docsNamedStyleSubtitle, + docsNamedStyleHeading1, docsNamedStyleHeading2, docsNamedStyleHeading3, + docsNamedStyleHeading4, docsNamedStyleHeading5, docsNamedStyleHeading6: return trimmed, nil default: return "", usage("--named-style must be one of NORMAL_TEXT, TITLE, SUBTITLE, HEADING_1..HEADING_6") diff --git a/internal/cmd/docs_format_headings_test.go b/internal/cmd/docs_format_headings_test.go index 11c3e2a6..834505ed 100644 --- a/internal/cmd/docs_format_headings_test.go +++ b/internal/cmd/docs_format_headings_test.go @@ -16,13 +16,13 @@ func TestDocsFormatFlags_HeadingLevelEmitsNamedStyleType(t *testing.T) { level int want string }{ - {"H1", 1, "HEADING_1"}, - {"H2", 2, "HEADING_2"}, - {"H6", 6, "HEADING_6"}, + {"H1", 1, docsNamedStyleHeading1}, + {"H2", 2, docsNamedStyleHeading2}, + {"H6", 6, docsNamedStyleHeading6}, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { - reqs, err := (DocsFormatFlags{HeadingLevel: tt.level}).buildRequests(3, 9, "") + reqs, err := (DocsFormatFlags{HeadingLevel: intPtr(tt.level)}).buildRequests(3, 9, "") if err != nil { t.Fatalf("buildRequests: %v", err) } @@ -41,7 +41,17 @@ func TestDocsFormatFlags_HeadingLevelEmitsNamedStyleType(t *testing.T) { } func TestDocsFormatFlags_NamedStyleAcceptsAllValidEnums(t *testing.T) { - for _, ns := range []string{"NORMAL_TEXT", "TITLE", "SUBTITLE", "HEADING_1", "HEADING_2", "HEADING_3", "HEADING_4", "HEADING_5", "HEADING_6"} { + for _, ns := range []string{ + docsNamedStyleNormalText, + docsNamedStyleTitle, + docsNamedStyleSubtitle, + docsNamedStyleHeading1, + docsNamedStyleHeading2, + docsNamedStyleHeading3, + docsNamedStyleHeading4, + docsNamedStyleHeading5, + docsNamedStyleHeading6, + } { t.Run(ns, func(t *testing.T) { reqs, err := (DocsFormatFlags{NamedStyle: ns}).buildRequests(1, 2, "") if err != nil { @@ -59,13 +69,13 @@ func TestDocsFormatFlags_NamedStyleIsCaseInsensitive(t *testing.T) { if err != nil { t.Fatalf("buildRequests: %v", err) } - if got := reqs[0].UpdateParagraphStyle.ParagraphStyle.NamedStyleType; got != "HEADING_3" { - t.Fatalf("NamedStyleType = %q, want HEADING_3", got) + if got := reqs[0].UpdateParagraphStyle.ParagraphStyle.NamedStyleType; got != docsNamedStyleHeading3 { + t.Fatalf("NamedStyleType = %q, want %s", got, docsNamedStyleHeading3) } } func TestDocsFormatFlags_HeadingLevelAndNamedStyleMutuallyExclusive(t *testing.T) { - _, err := (DocsFormatFlags{HeadingLevel: 1, NamedStyle: "TITLE"}).buildRequests(1, 2, "") + _, err := (DocsFormatFlags{HeadingLevel: intPtr(1), NamedStyle: "TITLE"}).buildRequests(1, 2, "") if err == nil || !strings.Contains(err.Error(), "cannot be combined") { t.Fatalf("expected mutual-exclusion error, got %v", err) } @@ -73,11 +83,7 @@ func TestDocsFormatFlags_HeadingLevelAndNamedStyleMutuallyExclusive(t *testing.T func TestDocsFormatFlags_HeadingLevelOutOfRangeRejected(t *testing.T) { for _, lvl := range []int{-1, 0, 7, 99} { - if lvl == 0 { - // 0 means "no heading level" — not an error on its own. - continue - } - _, err := (DocsFormatFlags{HeadingLevel: lvl}).buildRequests(1, 2, "") + _, err := (DocsFormatFlags{HeadingLevel: intPtr(lvl)}).buildRequests(1, 2, "") if err == nil || !strings.Contains(err.Error(), "--heading-level must be between 1 and 6") { t.Fatalf("level %d: expected range error, got %v", lvl, err) } @@ -92,7 +98,7 @@ func TestDocsFormatFlags_UnknownNamedStyleRejected(t *testing.T) { } func TestDocsFormatFlags_HeadingComposesWithAlignment(t *testing.T) { - reqs, err := (DocsFormatFlags{HeadingLevel: 1, Alignment: "center"}).buildRequests(3, 9, "t.tab") + reqs, err := (DocsFormatFlags{HeadingLevel: intPtr(1), Alignment: "center"}).buildRequests(3, 9, "t.tab") if err != nil { t.Fatalf("buildRequests: %v", err) } @@ -100,7 +106,7 @@ func TestDocsFormatFlags_HeadingComposesWithAlignment(t *testing.T) { t.Fatalf("expected one paragraph request combining alignment + heading, got %d", len(reqs)) } pr := reqs[0].UpdateParagraphStyle - if pr.ParagraphStyle.NamedStyleType != "HEADING_1" || pr.ParagraphStyle.Alignment != "CENTER" { + if pr.ParagraphStyle.NamedStyleType != docsNamedStyleHeading1 || pr.ParagraphStyle.Alignment != "CENTER" { t.Fatalf("unexpected style: %#v", pr.ParagraphStyle) } if !strings.Contains(pr.Fields, "namedStyleType") || !strings.Contains(pr.Fields, "alignment") { @@ -112,13 +118,15 @@ func TestDocsFormatFlags_HeadingComposesWithAlignment(t *testing.T) { } func TestDocsFormatFlags_AnyDetectsHeadingFlags(t *testing.T) { - if !(DocsFormatFlags{HeadingLevel: 2}).any() { + if !(DocsFormatFlags{HeadingLevel: intPtr(2)}).any() { t.Fatalf("any() should be true when HeadingLevel is set") } - if !(DocsFormatFlags{NamedStyle: "TITLE"}).any() { + if !(DocsFormatFlags{NamedStyle: docsNamedStyleTitle}).any() { t.Fatalf("any() should be true when NamedStyle is set") } if (DocsFormatFlags{}).any() { t.Fatalf("any() should be false when nothing is set") } } + +func intPtr(v int) *int { return &v }