mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
46e2186adf
Assembling the --slides JSON array by hand is what callers keep getting
wrong. A page of SML is multi-line and quote-heavy, and shell has no
built-in way to JSON-escape it, so callers reached for `jq -n --rawfile`
to build the array. In environments without jq the substitution silently
became an empty string and the command ran on to create an empty deck,
or the half-escaped XML reached the backend and came back as an opaque
3350001 after the presentation already existed.
Two input forms remove the escaping step:
--slides now declares Input{file, stdin}, so a finished array can be
read with `--slides @deck.json` or piped in with `--slides -`.
--slide is repeatable, takes one complete <slide> document (or @path),
and the CLI assembles the array. Repetition order is page order.
The forms are mutually exclusive: merging them would make page order
depend on flag-parsing rules nobody wants to reason about.
Notes on the repeatable flag: the framework only resolves Flag.Input for
single-valued string flags, so --slide resolves @path itself, through
the same cmdutil.ReadInputFile the framework uses, keeping the
"relative path under the current directory" rule identical. It rejects
"-" outright, because a process has one stdin and that cannot mean "this
occurrence" on a repeatable flag; the error names both forms that work.
Structural validation now runs on the assembled array, so both forms
fail the same way, and it runs before the create call so a malformed
page can no longer leave an orphaned empty presentation behind.
Three inputs the first round of review found still slipping through are
now rejected or normalized before the create call. `--slides null` is
valid JSON for a slice, so it parsed without error and left the array
nil, which read as "no pages given" and produced the blank deck
reported as success that the empty-value check exists to prevent. An
`<?xml ...?>` prolog is well-formed XML, so the parser accepted it and
only the backend rejected it, with 4001000 buildSnNode, after the
presentation already existed; it is now caught in the shared slide
validator, which covers +add-slide and +replace-pages too. And a
leading UTF-8 BOM made `--slide @page.xml` reject a file that
`--slides @deck.json` accepted, because the framework strips it for
Input flags and the repeatable flag resolved @path itself;
StripUTF8BOM is exported so both paths normalize the same way.
Also threads the source flag name through uploadSlidesPlaceholders,
which previously reported +add-slide upload failures as --slides.
Docs: the create/troubleshooting references now teach the file inputs
instead of the jq array-building template, and the follow-up snippet
uses the CLI's own --jq instead of piping to an external jq.
The lint gate in SKILL.md now names `slides +create` as a whole rather
than only its --slide form.
86 lines
2.7 KiB
Go
86 lines
2.7 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package slides
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"errors"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// Shared helpers for the whole-page slides commands. They lived in
|
|
// slides_replace_pages.go until that command was retired in favour of
|
|
// +update-slide; +add-slide and +delete-slide still use them.
|
|
|
|
// validateCompleteSlideXML checks that content is exactly one complete <slide>
|
|
// document: a single <slide> root, nothing but whitespace outside it, and every
|
|
// element closed. It reports the structural problem alone — callers re-tag the
|
|
// error with the flag it came from.
|
|
func validateCompleteSlideXML(content string) error {
|
|
dec := xml.NewDecoder(strings.NewReader(content))
|
|
depth := 0
|
|
seenRoot := false
|
|
for {
|
|
tok, err := dec.Token()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch t := tok.(type) {
|
|
case xml.StartElement:
|
|
if depth == 0 {
|
|
if seenRoot {
|
|
return invalidSlideXMLStructureError("multiple root elements")
|
|
}
|
|
if t.Name.Local != "slide" {
|
|
return invalidSlideXMLStructureError("root element is <%s>, want <slide>", t.Name.Local)
|
|
}
|
|
seenRoot = true
|
|
}
|
|
depth++
|
|
case xml.EndElement:
|
|
depth--
|
|
case xml.ProcInst:
|
|
// An `<?xml ...?>` prolog copied from a generic XML sample is
|
|
// well-formed, so nothing local used to object and it reached the
|
|
// backend, which answers 4001000 buildSnNode once the presentation
|
|
// already exists. Every caller of this validator posts to
|
|
// .../slide, and that is the endpoint that rejects it. Measured, so
|
|
// it does not get "made consistent" later: .../slide/replace takes
|
|
// the same prolog and applies the content, which is why
|
|
// +update-slide and +replace-slide stay permissive.
|
|
return invalidSlideXMLStructureError("<?%s ...?> declaration is not supported; remove it so the document starts with <slide>", t.Target)
|
|
case xml.CharData:
|
|
if depth == 0 && strings.TrimSpace(string(t)) != "" {
|
|
return invalidSlideXMLStructureError("non-whitespace text outside root element")
|
|
}
|
|
}
|
|
}
|
|
if !seenRoot {
|
|
return invalidSlideXMLStructureError("missing root element")
|
|
}
|
|
if depth != 0 {
|
|
return invalidSlideXMLStructureError("unclosed XML element")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func invalidSlideXMLStructureError(format string, args ...interface{}) error {
|
|
return errs.NewValidationError(errs.SubtypeInvalidArgument, format, args...)
|
|
}
|
|
|
|
// revisionFromData extracts revision_id from a response payload when present.
|
|
func revisionFromData(data map[string]interface{}) (int, bool) {
|
|
if _, ok := data["revision_id"]; !ok {
|
|
return 0, false
|
|
}
|
|
return int(common.GetFloat(data, "revision_id")), true
|
|
}
|