Skip to content

Commit 5ff811d

Browse files
authored
feat(slides): add structured introspection (#836)
Add structured Slides metadata and element inspection, table-aware text reading, and read-only UTF-16 text location with full docs and live provider proof.
1 parent fa3fa16 commit 5ff811d

18 files changed

Lines changed: 1417 additions & 58 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- Slides: allow `insert-image` and `replace-slide` to use public HTTPS image URLs without temporary Drive sharing. (#825) — thanks @sebsnyk.
8+
- Slides: add structured element geometry, styled text runs, table-cell content, image source URLs, native presentation metadata, and read-only text location with exact UTF-16 ranges. (#822) — thanks @sebsnyk.
89

910
### Fixed
1011

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,15 @@ gog sheets banding set <spreadsheetId> 'Sheet1!A1:D100'
381381

382382
Docs: [Slides from Markdown](docs/slides-markdown.md),
383383
[template replacement](docs/slides-template-replacement.md),
384+
[introspection](docs/slides-introspection.md),
384385
[`gog slides`](docs/commands/gog-slides.md),
385386
[`gog forms`](docs/commands/gog-forms.md).
386387

387388
```bash
388389
gog slides create-from-markdown "Weekly update" --content-file slides.md
390+
gog slides info <presentationId> --json
391+
gog slides read-slide <presentationId> <slideId> --detail --json
392+
gog slides locate <presentationId> "Quarterly revenue" --all --json
389393
gog slides insert-image <presentationId> <slideId> chart.png --x 24 --y 24 --width 240
390394
gog slides insert-text <presentationId> <objectId> "New text"
391395
gog forms update <formId> --quiz=true

docs/commands.generated.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,8 +596,9 @@ Generated from `gog schema --json`.
596596
- [`gog slides (slide) insert-image --width=FLOAT-64 <presentationId> <slideId> [<image>] [flags]`](commands/gog-slides-insert-image.md) - Insert a local or public image at a position and size
597597
- [`gog slides (slide) insert-text <presentationId> <objectId> <text> [flags]`](commands/gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
598598
- [`gog slides (slide) list-slides <presentationId>`](commands/gog-slides-list-slides.md) - List all slides with their object IDs
599+
- [`gog slides (slide) locate (find-element) <presentationId> <text> [flags]`](commands/gog-slides-locate.md) - Locate text in shapes and table cells with object IDs and UTF-16 ranges
599600
- [`gog slides (slide) raw <presentationId> [flags]`](commands/gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
600-
- [`gog slides (slide) read-slide <presentationId> <slideId>`](commands/gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
601+
- [`gog slides (slide) read-slide <presentationId> <slideId> [flags]`](commands/gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
601602
- [`gog slides (slide) replace-slide <presentationId> <slideId> [<image>] [flags]`](commands/gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL
602603
- [`gog slides (slide) replace-text <presentationId> <find> <replacement> [flags]`](commands/gog-slides-replace-text.md) - Find-and-replace text across a presentation
603604
- [`gog slides (slide) thumbnail (thumb) <presentationId> <slideId> [flags]`](commands/gog-slides-thumbnail.md) - Get or download a rendered thumbnail for a slide

docs/commands/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments.
44

5-
Generated pages: 646.
5+
Generated pages: 647.
66

77
## Top-level Commands
88

@@ -647,6 +647,7 @@ Generated pages: 646.
647647
- [gog slides insert-image](gog-slides-insert-image.md) - Insert a local or public image at a position and size
648648
- [gog slides insert-text](gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
649649
- [gog slides list-slides](gog-slides-list-slides.md) - List all slides with their object IDs
650+
- [gog slides locate](gog-slides-locate.md) - Locate text in shapes and table cells with object IDs and UTF-16 ranges
650651
- [gog slides raw](gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
651652
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
652653
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL

docs/commands/gog-slides-locate.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# `gog slides locate`
2+
3+
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
4+
5+
Locate text in shapes and table cells with object IDs and UTF-16 ranges
6+
7+
## Usage
8+
9+
```bash
10+
gog slides (slide) locate (find-element) <presentationId> <text> [flags]
11+
```
12+
13+
## Parent
14+
15+
- [gog slides](gog-slides.md)
16+
17+
## Flags
18+
19+
| Flag | Type | Default | Help |
20+
| --- | --- | --- | --- |
21+
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
22+
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email, alias, or auto for authenticated Google API commands |
23+
| `--all` | `bool` | | Return all matches |
24+
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
25+
| `--color` | `string` | auto | Color output: auto\|always\|never |
26+
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
27+
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
28+
| `--enable-commands` | `string` | | Comma-separated list of enabled command prefixes; dot paths allowed (restricts CLI) |
29+
| `--enable-commands-exact` | `string` | | Comma-separated list of exact enabled commands; dot paths allowed and parent commands do not enable children |
30+
| `--fail-empty`<br>`--non-empty`<br>`--require-results` | `bool` | | Exit with code 3 if no matches |
31+
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
32+
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
33+
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
34+
| `--home` | `string` | | Override gogcli config/data/state/cache root (equivalent to GOG_HOME) |
35+
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
36+
| `--match-case` | `bool` | | Use case-sensitive matching |
37+
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
38+
| `--occurrence` | `*int` | | Return the Nth occurrence (1-based; default first) |
39+
| `--page` | `string` | | Limit matches to one slide object ID |
40+
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
41+
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
42+
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
43+
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
44+
| `--version` | `kong.VersionFlag` | | Print version and exit |
45+
| `--wrap-untrusted` | `bool` | false | In JSON/raw output, wrap fetched text fields in external untrusted-content markers |
46+
47+
## See Also
48+
49+
- [gog slides](gog-slides.md)
50+
- [Command index](README.md)

docs/commands/gog-slides-read-slide.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Read slide content: speaker notes, text elements, and images
77
## Usage
88

99
```bash
10-
gog slides (slide) read-slide <presentationId> <slideId>
10+
gog slides (slide) read-slide <presentationId> <slideId> [flags]
1111
```
1212

1313
## Parent
@@ -22,6 +22,7 @@ gog slides (slide) read-slide <presentationId> <slideId>
2222
| `-a`<br>`--account`<br>`--acct` | `string` | | Account email, alias, or auto for authenticated Google API commands |
2323
| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
2424
| `--color` | `string` | auto | Color output: auto\|always\|never |
25+
| `--detail` | `bool` | | Include normalized element geometry, text runs/styles, paragraphs, and table cells |
2526
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
2627
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
2728
| `--enable-commands` | `string` | | Comma-separated list of enabled command prefixes; dot paths allowed (restricts CLI) |

docs/commands/gog-slides.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ gog slides (slide) <command> [flags]
2727
- [gog slides insert-image](gog-slides-insert-image.md) - Insert a local or public image at a position and size
2828
- [gog slides insert-text](gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
2929
- [gog slides list-slides](gog-slides-list-slides.md) - List all slides with their object IDs
30+
- [gog slides locate](gog-slides-locate.md) - Locate text in shapes and table cells with object IDs and UTF-16 ranges
3031
- [gog slides raw](gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
3132
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
3233
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace an existing slide image from a local file or public URL

docs/slides-introspection.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Slides introspection
2+
3+
Use the structured Slides commands when an edit needs stable object IDs or exact
4+
text ranges. `gog slides raw` remains the lossless API escape hatch.
5+
6+
## Presentation metadata
7+
8+
`slides info` combines Drive file metadata with Slides-native metadata:
9+
10+
```bash
11+
gog slides info <presentationId> --json
12+
```
13+
14+
The `presentation` object includes slide count, page size in points, locale,
15+
masters and their theme colors, and layouts.
16+
17+
## Slide structure
18+
19+
The default `read-slide` output includes shape text, table-cell text, image
20+
content/source URLs, and speaker notes. Add `--detail --json` for normalized
21+
elements:
22+
23+
```bash
24+
gog slides read-slide <presentationId> <slideId> --detail --json
25+
```
26+
27+
Detailed elements are flattened in visual hierarchy order. Group children carry
28+
`parentObjectId`. `geometry` is the element's axis-aligned bounding box in
29+
points after its group and element transforms are applied. Text runs retain the
30+
Slides API's UTF-16 `startIndex`/`endIndex`, style, links, paragraph style, and
31+
bullet metadata. Foreground/background colors are also normalized as `#RRGGBB`
32+
or `theme:NAME`; API-omitted inherited style fields remain omitted.
33+
34+
Rendered image `contentUrl` values are short-lived and requester-scoped.
35+
`sourceUrl` is included when the presentation retains the original insertion
36+
URL.
37+
38+
## Locate editable text
39+
40+
`slides locate` searches shapes and table cells without changing the deck:
41+
42+
```bash
43+
gog slides locate <presentationId> "Quarterly revenue" --all --json
44+
gog slides locate <presentationId> "Approved" --page <slideId> --match-case
45+
```
46+
47+
Each match includes the slide and element object IDs plus exact UTF-16 range.
48+
Table matches also include zero-based row and column indexes. By default only
49+
the first match is returned; use `--all` or `--occurrence N` to select results,
50+
and `--fail-empty` for automation that requires a match.

internal/cmd/execute_docs_slides_sheets_more_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"google.golang.org/api/docs/v1"
1515
"google.golang.org/api/drive/v3"
1616
"google.golang.org/api/option"
17+
"google.golang.org/api/slides/v1"
1718

1819
"github.com/steipete/gogcli/internal/app"
1920
)
@@ -50,6 +51,14 @@ func TestExecute_DocsSlidesSheets_CopyCreateInfoCat_JSON(t *testing.T) {
5051
},
5152
})
5253
return
54+
case r.Method == http.MethodGet && strings.HasPrefix(path, "/v1/presentations/"):
55+
w.Header().Set("Content-Type", "application/json")
56+
_ = json.NewEncoder(w).Encode(map[string]any{
57+
"presentationId": "p1",
58+
"title": "Slides 1",
59+
"slides": []any{map[string]any{"objectId": "slide1"}},
60+
})
61+
return
5362
case r.Method == http.MethodGet && strings.Contains(drivePath, "/files/d1") && !strings.HasSuffix(drivePath, "/copy"):
5463
w.Header().Set("Content-Type", "application/json")
5564
_ = json.NewEncoder(w).Encode(map[string]any{
@@ -140,6 +149,15 @@ func TestExecute_DocsSlidesSheets_CopyCreateInfoCat_JSON(t *testing.T) {
140149
t.Fatalf("NewDocsService: %v", err)
141150
}
142151

152+
slidesSvc, err := slides.NewService(context.Background(),
153+
option.WithoutAuthentication(),
154+
option.WithHTTPClient(srv.Client()),
155+
option.WithEndpoint(srv.URL+"/"),
156+
)
157+
if err != nil {
158+
t.Fatalf("NewSlidesService: %v", err)
159+
}
160+
143161
export := func(_ context.Context, _ *drive.Service, fileID, mimeType string) (*http.Response, error) {
144162
if fileID == "" || mimeType == "" {
145163
return nil, fmt.Errorf("invalid export request: file=%q mime=%q", fileID, mimeType)
@@ -156,6 +174,9 @@ func TestExecute_DocsSlidesSheets_CopyCreateInfoCat_JSON(t *testing.T) {
156174
Docs: func(context.Context, string) (*docs.Service, error) {
157175
return docSvc, nil
158176
},
177+
Slides: func(context.Context, string) (*slides.Service, error) {
178+
return slidesSvc, nil
179+
},
159180
Drive: stubDriveService(svc),
160181
DriveExport: export,
161182
}}

internal/cmd/info_via_drive.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"strings"
88

9+
"google.golang.org/api/drive/v3"
10+
911
"github.com/steipete/gogcli/internal/outfmt"
1012
"github.com/steipete/gogcli/internal/ui"
1113
)
@@ -19,24 +21,31 @@ type infoViaDriveOptions struct {
1921
const infoViaDriveDefaultKindLabel = "expected type"
2022

2123
func infoViaDrive(ctx context.Context, flags *RootFlags, opts infoViaDriveOptions, id string) error {
22-
u := ui.FromContext(ctx)
23-
account, err := requireAccount(flags)
24+
f, err := loadInfoViaDrive(ctx, flags, opts, id)
2425
if err != nil {
2526
return err
2627
}
28+
return writeInfoViaDrive(ctx, f)
29+
}
30+
31+
func loadInfoViaDrive(ctx context.Context, flags *RootFlags, opts infoViaDriveOptions, id string) (*drive.File, error) {
32+
account, err := requireAccount(flags)
33+
if err != nil {
34+
return nil, err
35+
}
2736

2837
argName := strings.TrimSpace(opts.ArgName)
2938
if argName == "" {
3039
argName = "id"
3140
}
3241
id = normalizeGoogleID(strings.TrimSpace(id))
3342
if id == "" {
34-
return usage(fmt.Sprintf("empty %s", argName))
43+
return nil, usage(fmt.Sprintf("empty %s", argName))
3544
}
3645

3746
svc, err := driveService(ctx, account)
3847
if err != nil {
39-
return err
48+
return nil, err
4049
}
4150

4251
f, err := svc.Files.Get(id).
@@ -45,23 +54,27 @@ func infoViaDrive(ctx context.Context, flags *RootFlags, opts infoViaDriveOption
4554
Context(ctx).
4655
Do()
4756
if err != nil {
48-
return err
57+
return nil, err
4958
}
5059
if f == nil {
51-
return errors.New("file not found")
60+
return nil, errors.New("file not found")
5261
}
5362
if opts.ExpectedMime != "" && f.MimeType != opts.ExpectedMime {
5463
label := strings.TrimSpace(opts.KindLabel)
5564
if label == "" {
5665
label = infoViaDriveDefaultKindLabel
5766
}
58-
return fmt.Errorf("file is not a %s (mimeType=%q)", label, f.MimeType)
67+
return nil, fmt.Errorf("file is not a %s (mimeType=%q)", label, f.MimeType)
5968
}
69+
return f, nil
70+
}
6071

72+
func writeInfoViaDrive(ctx context.Context, f *drive.File) error {
6173
if outfmt.IsJSON(ctx) {
6274
return outfmt.WriteJSON(ctx, stdoutWriter(ctx), map[string]any{strFile: f})
6375
}
6476

77+
u := ui.FromContext(ctx)
6578
u.Out().Linef("id\t%s", f.Id)
6679
u.Out().Linef("name\t%s", f.Name)
6780
u.Out().Linef("mime\t%s", f.MimeType)

0 commit comments

Comments
 (0)