feat(docs): add html export format (#141) (thanks @in-liberty420)
This commit is contained in:
parent
647f3f6441
commit
ffd8cd482b
@ -10,6 +10,7 @@
|
||||
- Sheets: add `add-tab`, `rename-tab`, and `delete-tab` commands for managing spreadsheet tabs, with delete dry-run/confirmation guardrails. (#309) — thanks @JulienMalige.
|
||||
- Docs: add `--tab-id` to editing commands so write/update/insert/delete/find-replace can target a specific Google Docs tab. (#330) — thanks @ignacioreyna.
|
||||
- Docs: add native Google Docs Markdown export via `docs export --format md`. (#282) — thanks @fprochazka.
|
||||
- Docs: add native Google Docs HTML export via `docs export --format html`. (#141) — thanks @in-liberty420.
|
||||
- Auth: add `auth add --redirect-uri` for manual/remote OAuth flows, so custom callback hosts can be reused across the printed auth URL, state cache, and code exchange. (#398) — thanks @salmonumbrella.
|
||||
- Auth: add `--extra-scopes` to `auth add` for appending custom OAuth scope URIs beyond the built-in service scopes. (#421) — thanks @peteradams2026.
|
||||
- Gmail: add `gmail labels rename` to rename user labels by ID or exact name, with system-label guards and wrong-case ID safety. (#391) — thanks @adam-zethraeus.
|
||||
|
||||
@ -1230,6 +1230,7 @@ gog docs export <docId> --format pdf --out ./doc.pdf
|
||||
gog docs export <docId> --format docx --out ./doc.docx
|
||||
gog docs export <docId> --format txt --out ./doc.txt
|
||||
gog docs export <docId> --format md --out ./doc.md
|
||||
gog docs export <docId> --format html --out ./doc.html
|
||||
|
||||
# Sed-style regex editing with Markdown formatting (sedmat)
|
||||
gog docs sed <docId> 's/pattern/replacement/g'
|
||||
|
||||
@ -16,7 +16,7 @@ Goal: one implementation for “export Google *Thing* via Drive”.
|
||||
|
||||
Each service command is a thin wrapper:
|
||||
|
||||
- `gog docs export <docId> --format pdf|docx|txt|md`
|
||||
- `gog docs export <docId> --format pdf|docx|txt|md|html`
|
||||
- `gog slides export <presentationId> --format pdf|pptx`
|
||||
- `gog sheets export <spreadsheetId> --format pdf|xlsx|csv`
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ type DocsCmd struct {
|
||||
type DocsExportCmd struct {
|
||||
DocID string `arg:"" name:"docId" help:"Doc ID"`
|
||||
Output OutputPathFlag `embed:""`
|
||||
Format string `name:"format" help:"Export format: pdf|docx|txt|md" default:"pdf"`
|
||||
Format string `name:"format" help:"Export format: pdf|docx|txt|md|html" default:"pdf"`
|
||||
}
|
||||
|
||||
func (c *DocsExportCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
|
||||
@ -45,6 +45,7 @@ const (
|
||||
mimePNG = "image/png"
|
||||
mimeTextPlain = "text/plain"
|
||||
mimeTextMarkdown = "text/markdown"
|
||||
mimeHTML = "text/html"
|
||||
extPDF = ".pdf"
|
||||
extCSV = ".csv"
|
||||
extXlsx = ".xlsx"
|
||||
@ -53,6 +54,7 @@ const (
|
||||
extPNG = ".png"
|
||||
extTXT = ".txt"
|
||||
extMD = ".md"
|
||||
extHTML = ".html"
|
||||
formatAuto = "auto"
|
||||
driveShareToAnyone = "anyone"
|
||||
driveShareToUser = "user"
|
||||
@ -1220,10 +1222,10 @@ func validateDriveDownloadFormatFlag(format string) error {
|
||||
return nil
|
||||
}
|
||||
switch format {
|
||||
case "pdf", "csv", "xlsx", "pptx", "txt", "png", "docx", "md":
|
||||
case "pdf", "csv", "xlsx", "pptx", "txt", "png", "docx", "md", "html":
|
||||
return nil
|
||||
default:
|
||||
return usagef("invalid --format %q (use pdf|csv|xlsx|pptx|txt|png|docx|md)", format)
|
||||
return usagef("invalid --format %q (use pdf|csv|xlsx|pptx|txt|png|docx|md|html)", format)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1286,8 +1288,10 @@ func driveExportMimeTypeForFormat(googleMimeType string, format string) (string,
|
||||
return mimeTextPlain, nil
|
||||
case "md":
|
||||
return mimeTextMarkdown, nil
|
||||
case "html":
|
||||
return mimeHTML, nil
|
||||
default:
|
||||
return "", fmt.Errorf("invalid --format %q for Google Doc (use pdf|docx|txt|md)", format)
|
||||
return "", fmt.Errorf("invalid --format %q for Google Doc (use pdf|docx|txt|md|html)", format)
|
||||
}
|
||||
case driveMimeGoogleSheet:
|
||||
switch format {
|
||||
@ -1344,6 +1348,8 @@ func driveExportExtension(mimeType string) string {
|
||||
return extTXT
|
||||
case mimeTextMarkdown:
|
||||
return extMD
|
||||
case mimeHTML:
|
||||
return extHTML
|
||||
default:
|
||||
return extPDF
|
||||
}
|
||||
|
||||
@ -58,6 +58,12 @@ func TestDriveExportMimeTypeForFormat(t *testing.T) {
|
||||
format: "md",
|
||||
wantMime: "text/markdown",
|
||||
},
|
||||
{
|
||||
name: "doc_html",
|
||||
googleMime: "application/vnd.google-apps.document",
|
||||
format: "html",
|
||||
wantMime: "text/html",
|
||||
},
|
||||
{
|
||||
name: "doc_invalid",
|
||||
googleMime: "application/vnd.google-apps.document",
|
||||
|
||||
@ -54,6 +54,9 @@ func TestDriveExportExtension(t *testing.T) {
|
||||
if got := driveExportExtension("text/markdown"); got != ".md" {
|
||||
t.Fatalf("unexpected: %q", got)
|
||||
}
|
||||
if got := driveExportExtension("text/html"); got != ".html" {
|
||||
t.Fatalf("unexpected: %q", got)
|
||||
}
|
||||
if got := driveExportExtension("nope"); got != ".pdf" {
|
||||
t.Fatalf("unexpected: %q", got)
|
||||
}
|
||||
|
||||
@ -175,6 +175,86 @@ func TestExecute_DocsExport_Markdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_DocsExport_HTML(t *testing.T) {
|
||||
origNew := newDriveService
|
||||
origExport := driveExportDownload
|
||||
t.Cleanup(func() {
|
||||
newDriveService = origNew
|
||||
driveExportDownload = origExport
|
||||
})
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet || !strings.Contains(r.URL.Path, "/files/id1") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "id1",
|
||||
"name": "Doc",
|
||||
"mimeType": "application/vnd.google-apps.document",
|
||||
})
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
svc, err := drive.NewService(context.Background(),
|
||||
option.WithoutAuthentication(),
|
||||
option.WithHTTPClient(srv.Client()),
|
||||
option.WithEndpoint(srv.URL+"/"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("NewService: %v", err)
|
||||
}
|
||||
newDriveService = func(context.Context, string) (*drive.Service, error) { return svc, nil }
|
||||
|
||||
var gotExportMime string
|
||||
driveExportDownload = func(_ context.Context, _ *drive.Service, _ string, mimeType string) (*http.Response, error) {
|
||||
gotExportMime = mimeType
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Status: "200 OK",
|
||||
Body: io.NopCloser(strings.NewReader("<h1>Doc</h1>\n")),
|
||||
}, nil
|
||||
}
|
||||
|
||||
outBase := filepath.Join(t.TempDir(), "out")
|
||||
|
||||
stdout := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if execErr := Execute([]string{
|
||||
"--json",
|
||||
"--account", "a@b.com",
|
||||
"docs", "export", "id1",
|
||||
"--out", outBase,
|
||||
"--format", "html",
|
||||
}); execErr != nil {
|
||||
t.Fatalf("Execute: %v", execErr)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var parsed struct {
|
||||
Path string `json:"path"`
|
||||
Size int64 `json:"size"`
|
||||
}
|
||||
if unmarshalErr := json.Unmarshal([]byte(stdout), &parsed); unmarshalErr != nil {
|
||||
t.Fatalf("json parse: %v\nout=%q", unmarshalErr, stdout)
|
||||
}
|
||||
if want := outBase + ".html"; parsed.Path != want || parsed.Size != 13 {
|
||||
t.Fatalf("unexpected: %#v", parsed)
|
||||
}
|
||||
if gotExportMime != "text/html" {
|
||||
t.Fatalf("unexpected export mime type: %q", gotExportMime)
|
||||
}
|
||||
b, err := os.ReadFile(outBase + ".html")
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile: %v", err)
|
||||
}
|
||||
if string(b) != "<h1>Doc</h1>\n" {
|
||||
t.Fatalf("unexpected file contents: %q", string(b))
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecute_DocsExport_TypeMismatch(t *testing.T) {
|
||||
origNew := newDriveService
|
||||
t.Cleanup(func() { newDriveService = origNew })
|
||||
|
||||
Loading…
Reference in New Issue
Block a user