mirror of
https://github.com/larksuite/cli.git
synced 2026-09-14 18:42:53 +08:00
b20f374c18
XML written into a field name this shortcut does not accept — most often "content", because <shape> nests a <content> child — was silently dropped, so the part failed the required-field check and reported "requires non-empty replacement". That reads as "the value is empty", which sends callers rewriting the value instead of the key. Reject fields outside the action's own set and name the field the caller most likely meant, with a correct one-liner attached as a hint. Matching folds case and separators so "Content", "newXml" and "block-id" resolve too, while the whitelist itself stays exact: the API accepts only snake_case, so "Replacement" must be rejected rather than slip through. Only block_replace and block_insert parts are checked, so missing / str_replace / unknown actions keep their existing errors, and an actually-empty payload still reports the non-empty wording. The alias list covers only names that plausibly carry a fragment. A shape attribute like "fill" is deliberately absent: whoever writes it means "recolor this block", not "here is my XML", so answering did-you-mean "replacement" would be guessing. The unknown-field error already names the valid set, which is true under either reading. Docs carry the same constraint at the three points a caller can hit first: SKILL.md, the +replace-slide reference (warning + counter-examples + error table), and the read-modify-write workflow. The --parts flag description now spells the field names out instead of eliding them behind "...". Note: this tightens parsing. Extra keys inside a part used to be ignored; they are now rejected.
497 lines
20 KiB
Go
497 lines
20 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||
// SPDX-License-Identifier: MIT
|
||
|
||
package slides
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"slices"
|
||
"strings"
|
||
|
||
"github.com/larksuite/cli/errs"
|
||
"github.com/larksuite/cli/shortcuts/common"
|
||
)
|
||
|
||
// maxReplaceParts matches the server-side cap declared in meta_data.json
|
||
// ("最少1条,最多200条"). Enforced client-side so a too-large batch fails fast
|
||
// with a clear message instead of a 400 from the backend.
|
||
const maxReplaceParts = 200
|
||
|
||
// SlidesReplaceSlide wraps slides.xml_presentation.slide.replace with specific
|
||
// value-adds over the raw auto-generated command:
|
||
//
|
||
// 1. It accepts --presentation as token / slides URL / wiki URL (and resolves
|
||
// wiki tokens), same as other slides shortcuts.
|
||
// 2. For every `block_replace` part it auto-injects `id="<block_id>"` into the
|
||
// root element of `replacement`. The backend requires the replacement
|
||
// fragment's root carry that id and returns 3350001 otherwise; the
|
||
// requirement is undocumented and catches callers repeatedly, so we fix it
|
||
// at the CLI layer.
|
||
// 3. For `<shape>` elements it auto-injects `<content/>` when missing. The
|
||
// SML 2.0 schema requires every shape to carry a content child; omitting
|
||
// it triggers 3350001.
|
||
// 4. On 3350001 errors it enriches the hint with context-specific guidance
|
||
// so AI agents can self-correct.
|
||
//
|
||
// `str_replace` is intentionally NOT exposed: product direction is that
|
||
// slide edits go through structural (block-level) operations only. The backend
|
||
// still accepts str_replace, but the CLI rejects it up front.
|
||
var SlidesReplaceSlide = common.Shortcut{
|
||
Service: "slides",
|
||
Command: "+replace-slide",
|
||
Description: "Replace elements on a slide via block_replace / block_insert parts (auto-injects id + <content/> on shape elements)",
|
||
Risk: "write",
|
||
Scopes: []string{"slides:presentation:update", "slides:presentation:write_only"},
|
||
// wiki:node:read is required only when --presentation is a wiki URL.
|
||
ConditionalScopes: []string{"wiki:node:read"},
|
||
AuthTypes: []string{"user", "bot"},
|
||
Flags: []common.Flag{
|
||
requiredPresentationRefFlag(),
|
||
{Name: "slide-id", Desc: "slide page identifier (slide_id)", Required: true},
|
||
{Name: "parts", Desc: "JSON array of replace parts; each needs action plus block_id + replacement (block_replace) or insertion [+ insert_before_block_id] (block_insert); any other key is rejected; max 200", Required: true, Input: []string{common.File, common.Stdin}},
|
||
{Name: "revision-id", Type: "int", Default: "-1", Desc: "presentation revision (-1 = latest; pass a specific number for optimistic locking)"},
|
||
{Name: "tid", Desc: "transaction id for concurrent-edit locking (usually empty)"},
|
||
},
|
||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||
ref, err := parsePresentationRef(runtime.Str("presentation"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if ref.Kind == "wiki" {
|
||
if err := runtime.EnsureScopes([]string{"wiki:node:read"}); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if strings.TrimSpace(runtime.Str("slide-id")) == "" {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--slide-id cannot be empty").WithParam("--slide-id")
|
||
}
|
||
parts, err := parseReplaceParts(runtime.Str("parts"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := validateReplaceParts(parts); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
},
|
||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||
ref, err := parsePresentationRef(runtime.Str("presentation"))
|
||
if err != nil {
|
||
return common.NewDryRunAPI().Set("error", err.Error())
|
||
}
|
||
parts, err := parseReplaceParts(runtime.Str("parts"))
|
||
if err != nil {
|
||
return common.NewDryRunAPI().Set("error", err.Error())
|
||
}
|
||
if err := validateReplaceParts(parts); err != nil {
|
||
return common.NewDryRunAPI().Set("error", err.Error())
|
||
}
|
||
// Apply the same id-injection the real Execute does, so dry-run body
|
||
// shows what will actually be sent.
|
||
injected, err := injectBlockReplaceIDs(parts)
|
||
if err != nil {
|
||
return common.NewDryRunAPI().Set("error", err.Error())
|
||
}
|
||
|
||
slideID := runtime.Str("slide-id")
|
||
query := map[string]interface{}{
|
||
"slide_id": slideID,
|
||
"revision_id": runtime.Int("revision-id"),
|
||
}
|
||
if tid := runtime.Str("tid"); tid != "" {
|
||
query["tid"] = tid
|
||
}
|
||
body := map[string]interface{}{"parts": injected}
|
||
|
||
dry := common.NewDryRunAPI()
|
||
presentationID := ref.Token
|
||
if ref.Kind == "wiki" {
|
||
presentationID = "<resolved_slides_token>"
|
||
dry.Desc("2-step orchestration: resolve wiki → replace slide parts").
|
||
GET("/open-apis/wiki/v2/spaces/get_node").
|
||
Desc("[1] Resolve wiki node to slides presentation").
|
||
Params(map[string]interface{}{"token": ref.Token})
|
||
} else {
|
||
dry.Desc(fmt.Sprintf("Replace %d part(s) on slide %s", len(parts), slideID))
|
||
}
|
||
dry.POST(slideReplaceAPIPath(presentationID)).
|
||
Params(query).
|
||
Body(body)
|
||
return dry.Set("parts_count", len(parts))
|
||
},
|
||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||
ref, err := parsePresentationRef(runtime.Str("presentation"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
presentationID, err := resolvePresentationID(runtime, ref)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
slideID := strings.TrimSpace(runtime.Str("slide-id"))
|
||
|
||
parts, err := parseReplaceParts(runtime.Str("parts"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if err := validateReplaceParts(parts); err != nil {
|
||
return err
|
||
}
|
||
injected, err := injectBlockReplaceIDs(parts)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
query := map[string]interface{}{
|
||
"slide_id": slideID,
|
||
"revision_id": runtime.Int("revision-id"),
|
||
}
|
||
if tid := strings.TrimSpace(runtime.Str("tid")); tid != "" {
|
||
query["tid"] = tid
|
||
}
|
||
body := map[string]interface{}{"parts": injected}
|
||
|
||
data, err := runtime.CallAPITyped("POST", slideReplaceAPIPath(presentationID), query, body)
|
||
if err != nil {
|
||
return enrichSlidesReplaceError(err)
|
||
}
|
||
|
||
result := map[string]interface{}{
|
||
"xml_presentation_id": presentationID,
|
||
"slide_id": slideID,
|
||
"parts_count": len(injected),
|
||
}
|
||
// Presence check (not `v > 0`) mirrors the failed_part_index / failed_reason
|
||
// branches below, so behavior stays consistent across the three fields.
|
||
if _, ok := data["revision_id"]; ok {
|
||
result["revision_id"] = int(common.GetFloat(data, "revision_id"))
|
||
}
|
||
// Backend reports partial failures via failed_part_index / failed_reason.
|
||
// Surface them untouched so the caller can react.
|
||
if raw, ok := data["failed_part_index"]; ok {
|
||
result["failed_part_index"] = raw
|
||
}
|
||
if raw, ok := data["failed_reason"]; ok {
|
||
result["failed_reason"] = raw
|
||
}
|
||
|
||
runtime.Out(result, nil)
|
||
return nil
|
||
},
|
||
}
|
||
|
||
// replacePart is the normalized (post-JSON) representation of one entry in the
|
||
// parts array. Fields are nullable so we can tell "not provided" from "empty".
|
||
type replacePart struct {
|
||
Action string
|
||
Replacement *string
|
||
BlockID *string
|
||
Insertion *string
|
||
InsertBeforeBlockID *string
|
||
}
|
||
|
||
// parseReplaceParts decodes the --parts JSON into typed structs.
|
||
//
|
||
// Fields outside the action's own set are rejected (see
|
||
// checkReplacePartFields) rather than dropped: silently ignoring them is what
|
||
// made XML in a hallucinated field (e.g. "content") surface as the misleading
|
||
// "requires non-empty replacement". Parts carrying an action this shortcut
|
||
// doesn't expose keep their own errors from validateReplaceParts.
|
||
func parseReplaceParts(raw string) ([]replacePart, error) {
|
||
s := strings.TrimSpace(raw)
|
||
if s == "" {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts cannot be empty").WithParam("--parts")
|
||
}
|
||
var decoded []map[string]interface{}
|
||
if err := json.Unmarshal([]byte(s), &decoded); err != nil {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts invalid JSON, must be an array of objects: %v", err).WithParam("--parts").WithCause(err)
|
||
}
|
||
out := make([]replacePart, 0, len(decoded))
|
||
for i, m := range decoded {
|
||
p := replacePart{}
|
||
if v, ok := m["action"]; ok {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].action must be a string", i).WithParam("--parts")
|
||
}
|
||
p.Action = s
|
||
} else if err := checkMisspelledAction(i, m); err != nil {
|
||
// "Action" selects no schema, so the per-action check below would skip
|
||
// the part entirely and validateReplaceParts would only say action is
|
||
// required. Name the misspelling instead.
|
||
return nil, err
|
||
}
|
||
if err := checkReplacePartFields(i, m, p.Action); err != nil {
|
||
return nil, err
|
||
}
|
||
if v, ok := m["replacement"]; ok {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].replacement must be a string", i).WithParam("--parts")
|
||
}
|
||
p.Replacement = &s
|
||
}
|
||
if v, ok := m["block_id"]; ok {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].block_id must be a string", i).WithParam("--parts")
|
||
}
|
||
p.BlockID = &s
|
||
}
|
||
if v, ok := m["insertion"]; ok {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].insertion must be a string", i).WithParam("--parts")
|
||
}
|
||
p.Insertion = &s
|
||
}
|
||
if v, ok := m["insert_before_block_id"]; ok {
|
||
s, ok := v.(string)
|
||
if !ok {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].insert_before_block_id must be a string", i).WithParam("--parts")
|
||
}
|
||
p.InsertBeforeBlockID = &s
|
||
}
|
||
out = append(out, p)
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// replacePartSchema describes one exposed action: the fields it accepts, the
|
||
// field carrying its XML fragment, and a correct one-liner to echo back.
|
||
type replacePartSchema struct {
|
||
fields []string
|
||
payload string
|
||
hint string
|
||
}
|
||
|
||
// replacePartSchemas is the field contract of the two exposed actions. Holding
|
||
// it in one place lets the error name the exact set the caller may use, instead
|
||
// of a generic "unknown field".
|
||
var replacePartSchemas = map[string]replacePartSchema{
|
||
"block_replace": {
|
||
fields: []string{"action", "block_id", "replacement"},
|
||
payload: "replacement",
|
||
hint: `correct shape: {"action":"block_replace","block_id":"bUn","replacement":"<shape type=\"text\"><content><p>text</p></content></shape>"}`,
|
||
},
|
||
"block_insert": {
|
||
fields: []string{"action", "insertion", "insert_before_block_id"},
|
||
payload: "insertion",
|
||
hint: `correct shape: {"action":"block_insert","insertion":"<shape type=\"rect\" width=\"100\" height=\"100\"/>"}`,
|
||
},
|
||
}
|
||
|
||
// xmlPayloadAliases are the wrong field names callers reach for when they mean
|
||
// the XML payload. internal/suggest can't route these: edit distance misses
|
||
// most of them outright (content → replacement), and its prefix ranking would
|
||
// send block / block_xml to block_id — the wrong field. Hence the explicit
|
||
// list. "content" dominates in practice because <shape> nests a <content>
|
||
// child, which makes it the intuitive-but-wrong top-level key.
|
||
//
|
||
// Entries are limited to names actually observed carrying a fragment. A
|
||
// shape attribute like "fill" is deliberately absent: whoever writes it means
|
||
// "recolor this block", not "here is my XML", so pointing them at replacement
|
||
// would be guessing. The unknown-field error already names the valid set.
|
||
var xmlPayloadAliases = []string{"block", "block_xml", "content", "data", "element", "new_xml", "xml"}
|
||
|
||
// normalizeFieldKey folds case and the _/- separators so "Content", "newXml",
|
||
// "new-xml" and "new_xml" collapse onto one key. Callers pick the casing of
|
||
// whatever language they happen to be thinking in (the CLI's own flags are
|
||
// kebab-case), and the suggestion should survive that; the whitelist itself
|
||
// stays exact, because the API only accepts snake_case.
|
||
func normalizeFieldKey(k string) string {
|
||
k = strings.ReplaceAll(k, "_", "")
|
||
k = strings.ReplaceAll(k, "-", "")
|
||
return strings.ToLower(k)
|
||
}
|
||
|
||
// matchNormalized returns the candidate that matches k once both are
|
||
// normalized, so "blockId" resolves to "block_id".
|
||
func matchNormalized(k string, candidates []string) (string, bool) {
|
||
nk := normalizeFieldKey(k)
|
||
for _, c := range candidates {
|
||
if normalizeFieldKey(c) == nk {
|
||
return c, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// checkMisspelledAction runs when a part has no exact "action" key: if some key
|
||
// normalizes to it, that spelling is the real problem, and saying so beats the
|
||
// bare "action is required" the caller would otherwise get.
|
||
func checkMisspelledAction(i int, m map[string]interface{}) error {
|
||
keys := make([]string, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
slices.Sort(keys)
|
||
for _, k := range keys {
|
||
if normalizeFieldKey(k) != "action" {
|
||
continue
|
||
}
|
||
err := errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||
"--parts[%d] unknown field %q; did you mean %q?", i, k, "action",
|
||
).WithParam("--parts")
|
||
// The misspelled key still holds the action name, so the example can be
|
||
// the one the caller was actually reaching for.
|
||
if name, ok := m[k].(string); ok {
|
||
if schema, known := replacePartSchemas[name]; known {
|
||
return err.WithHint("%s", schema.hint)
|
||
}
|
||
}
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// checkReplacePartFields rejects fields that don't belong to the part's action,
|
||
// naming the field the caller most likely meant. Actions this shortcut doesn't
|
||
// expose ("", str_replace, unknown) are skipped so their own errors from
|
||
// validateReplaceParts still win.
|
||
func checkReplacePartFields(i int, m map[string]interface{}, action string) error {
|
||
schema, ok := replacePartSchemas[action]
|
||
if !ok {
|
||
return nil
|
||
}
|
||
keys := make([]string, 0, len(m))
|
||
for k := range m {
|
||
keys = append(keys, k)
|
||
}
|
||
// Sorted so a part carrying several bad fields always reports the same one.
|
||
slices.Sort(keys)
|
||
for _, k := range keys {
|
||
if slices.Contains(schema.fields, k) {
|
||
continue
|
||
}
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument,
|
||
"%s", unknownPartFieldMessage(i, k, action, schema),
|
||
).WithParam("--parts").WithHint("%s", schema.hint)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// unknownPartFieldMessage builds the error text for one rejected field,
|
||
// naming the field the caller most likely meant.
|
||
func unknownPartFieldMessage(i int, key, action string, schema replacePartSchema) string {
|
||
valid := fmt.Sprintf("Valid fields for %s: %s", action, strings.Join(schema.fields, ", "))
|
||
// Right field, wrong casing or separator (e.g. "blockId", "block-id").
|
||
if field, ok := matchNormalized(key, schema.fields); ok {
|
||
return fmt.Sprintf("--parts[%d] unknown field %q; did you mean %q? %s", i, key, field, valid)
|
||
}
|
||
if _, ok := matchNormalized(key, xmlPayloadAliases); ok {
|
||
return fmt.Sprintf("--parts[%d] unknown field %q; did you mean %q? %s", i, key, schema.payload, valid)
|
||
}
|
||
// Other actions are checked in sorted-name order so the one named in the
|
||
// error stays deterministic even if a future field matches several.
|
||
others := make([]string, 0, len(replacePartSchemas))
|
||
for name := range replacePartSchemas {
|
||
if name != action {
|
||
others = append(others, name)
|
||
}
|
||
}
|
||
slices.Sort(others)
|
||
for _, other := range others {
|
||
if _, ok := matchNormalized(key, replacePartSchemas[other].fields); ok {
|
||
return fmt.Sprintf("--parts[%d] unknown field %q; it belongs to %s. %s", i, key, other, valid)
|
||
}
|
||
}
|
||
return fmt.Sprintf("--parts[%d] unknown field %q. %s", i, key, valid)
|
||
}
|
||
|
||
const larkCodeSlidesInvalidParam = 3350001
|
||
|
||
// slides3350001Hint is the generic checklist attached to 3350001 errors.
|
||
// 3350001 is a catch-all on the backend; listing the common root causes gives
|
||
// AI agents and humans a concrete starting point. Mixed block_replace+block_insert
|
||
// batches are supported, so splitting them is deliberately NOT suggested.
|
||
const slides3350001Hint = "common causes: (1) block_id not found in current slide — re-run slide.get for latest XML; (2) invalid XML structure or unsupported element; (3) element coordinates exceed slide bounds (960×540)"
|
||
|
||
// enrichSlidesReplaceError attaches slides3350001Hint when the API returns
|
||
// 3350001 (invalid param). Other error codes pass through untouched.
|
||
func enrichSlidesReplaceError(err error) error {
|
||
p, ok := errs.ProblemOf(err)
|
||
if !ok || p.Code != larkCodeSlidesInvalidParam {
|
||
return err
|
||
}
|
||
// Only fall back to the generic checklist when no upstream hint is
|
||
// already attached — don't clobber a more specific hint set by the
|
||
// backend or an earlier wrapper. p points at the embedded Problem, so
|
||
// the mutation is reflected in the returned err.
|
||
if p.Hint == "" {
|
||
p.Hint = slides3350001Hint
|
||
}
|
||
return err
|
||
}
|
||
|
||
// validateReplaceParts enforces CLI-level invariants:
|
||
// - size is within [1, 200]
|
||
// - action is one of the exposed actions (block_replace / block_insert)
|
||
// - per-action required fields are present
|
||
func validateReplaceParts(parts []replacePart) error {
|
||
if len(parts) == 0 {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts must contain at least 1 item").WithParam("--parts")
|
||
}
|
||
if len(parts) > maxReplaceParts {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts contains %d items, exceeds maximum of %d", len(parts), maxReplaceParts).WithParam("--parts")
|
||
}
|
||
for i, p := range parts {
|
||
switch p.Action {
|
||
case "block_replace":
|
||
if p.BlockID == nil || strings.TrimSpace(*p.BlockID) == "" {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d] (block_replace) requires non-empty block_id", i).WithParam("--parts")
|
||
}
|
||
if p.Replacement == nil || strings.TrimSpace(*p.Replacement) == "" {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d] (block_replace) requires non-empty replacement", i).WithParam("--parts")
|
||
}
|
||
case "block_insert":
|
||
if p.Insertion == nil || strings.TrimSpace(*p.Insertion) == "" {
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d] (block_insert) requires non-empty insertion", i).WithParam("--parts")
|
||
}
|
||
case "str_replace":
|
||
// Backend still accepts str_replace, but product decision is to
|
||
// force structural edits through the CLI. Block it up-front so
|
||
// users don't build tooling around an option we won't keep.
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d] action %q is not supported by this shortcut; use block_replace or block_insert", i, p.Action).WithParam("--parts")
|
||
case "":
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].action is required", i).WithParam("--parts")
|
||
default:
|
||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d] unknown action %q, supported: block_replace, block_insert", i, p.Action).WithParam("--parts")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// injectBlockReplaceIDs rewrites each block_replace part's `replacement` so
|
||
// that the root element carries id="<block_id>". Backend (3350001) requires
|
||
// this; doing it in the CLI means users write natural-looking XML (e.g.
|
||
// `<shape type="rect">…</shape>`) and get the id stitched in automatically.
|
||
//
|
||
// Returns a slice of `map[string]interface{}` ready to be encoded as the
|
||
// request body, preserving field order handed to the JSON encoder.
|
||
func injectBlockReplaceIDs(parts []replacePart) ([]map[string]interface{}, error) {
|
||
out := make([]map[string]interface{}, 0, len(parts))
|
||
for i, p := range parts {
|
||
m := map[string]interface{}{"action": p.Action}
|
||
switch p.Action {
|
||
case "block_replace":
|
||
fixed, err := ensureXMLRootID(*p.Replacement, *p.BlockID)
|
||
if err != nil {
|
||
return nil, errs.NewValidationError(errs.SubtypeInvalidArgument, "--parts[%d].replacement: %v", i, err).WithParam("--parts").WithCause(err)
|
||
}
|
||
fixed = ensureShapeHasContent(fixed)
|
||
m["block_id"] = *p.BlockID
|
||
m["replacement"] = fixed
|
||
case "block_insert":
|
||
m["insertion"] = ensureShapeHasContent(*p.Insertion)
|
||
if p.InsertBeforeBlockID != nil {
|
||
m["insert_before_block_id"] = *p.InsertBeforeBlockID
|
||
}
|
||
}
|
||
out = append(out, m)
|
||
}
|
||
return out, nil
|
||
}
|