Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions shortcuts/common/selection_normalize.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package common

import "strings"

// NormalizeSelectionWithEllipsis returns a canonical form of a user-typed
// --selection-with-ellipsis value suitable for server-side matching, along
// with a flag indicating whether any rewrite happened.
//
// The Lark docx store keeps punctuation in a canonical shape — straight ASCII
// quotes, LF line endings — while user-provided selection strings often come
// from pasted prose that has been auto-corrected to curly quotes, CRLF, or
// other typographic variants. Matching is strict byte-level, so a curly/
// straight mismatch on a single character is enough to defeat the whole
// selection.
//
// The normalization set is deliberately conservative: only transformations
// that are virtually always safe (typographic quotes and CR line endings)
// are applied. Full/half-width Latin punctuation or CJK punctuation is left
// alone, since those can legitimately appear verbatim in the document body.
func NormalizeSelectionWithEllipsis(s string) (string, bool) {
if s == "" {
return s, false
}
out := s
// Curly single quotes → ASCII apostrophe.
out = strings.ReplaceAll(out, "\u2018", "'")
out = strings.ReplaceAll(out, "\u2019", "'")
// Curly double quotes → ASCII double quote.
out = strings.ReplaceAll(out, "\u201C", "\"")
out = strings.ReplaceAll(out, "\u201D", "\"")
// CRLF / standalone CR → LF. Lark stores LF internally; sending CRLF in
// a selection would require the document to contain literal CR bytes,
// which it never does.
out = strings.ReplaceAll(out, "\r\n", "\n")
out = strings.ReplaceAll(out, "\r", "\n")
return out, out != s
}
96 changes: 96 additions & 0 deletions shortcuts/common/selection_normalize_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

package common

import "testing"

func TestNormalizeSelectionWithEllipsis(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
want string
wantChanged bool
}{
{
name: "empty passes through",
input: "",
want: "",
wantChanged: false,
},
{
name: "cjk-only selection is untouched",
input: "欢迎大家多给反馈",
want: "欢迎大家多给反馈",
wantChanged: false,
},
Comment thread
coderabbitai[bot] marked this conversation as resolved.
{
name: "ascii-only selection is untouched",
input: "hello world",
want: "hello world",
wantChanged: false,
},
{
name: "curly single quotes normalized",
input: "\u2018That\u2019s All\u2019",
want: "'That's All'",
wantChanged: true,
},
{
name: "curly double quotes normalized",
input: "he said \u201Chello\u201D",
want: "he said \"hello\"",
wantChanged: true,
},
{
name: "mixed curly + straight normalized",
input: "start\u2019s...end",
want: "start's...end",
wantChanged: true,
},
{
name: "crlf collapsed to lf",
input: "line1\r\nline2",
want: "line1\nline2",
wantChanged: true,
},
{
name: "standalone cr collapsed to lf",
input: "line1\rline2",
want: "line1\nline2",
wantChanged: true,
},
{
name: "already lf is untouched",
input: "line1\nline2",
want: "line1\nline2",
wantChanged: false,
},
{
name: "chinese punctuation deliberately untouched",
input: "你好,世界",
want: "你好,世界",
wantChanged: false,
},
{
name: "fullwidth latin deliberately untouched",
input: "ABC",
want: "ABC",
wantChanged: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got, changed := NormalizeSelectionWithEllipsis(tt.input)
if got != tt.want {
t.Errorf("NormalizeSelectionWithEllipsis(%q) = %q, want %q", tt.input, got, tt.want)
}
if changed != tt.wantChanged {
t.Errorf("NormalizeSelectionWithEllipsis(%q) changed=%v, want %v", tt.input, changed, tt.wantChanged)
}
})
}
}
4 changes: 2 additions & 2 deletions shortcuts/doc/doc_media_insert.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ var DocMediaInsert = common.Shortcut{
}
mediaType := runtime.Str("type")
caption := runtime.Str("caption")
selection := strings.TrimSpace(runtime.Str("selection-with-ellipsis"))
selection, _ := common.NormalizeSelectionWithEllipsis(strings.TrimSpace(runtime.Str("selection-with-ellipsis")))
hasSelection := selection != ""
fileViewType := fileViewMap[runtime.Str("file-view")]

Expand Down Expand Up @@ -251,7 +251,7 @@ var DocMediaInsert = common.Shortcut{
}
fmt.Fprintf(runtime.IO().ErrOut, "Root block ready: %s (%d children)\n", parentBlockID, insertIndex)

selection := strings.TrimSpace(runtime.Str("selection-with-ellipsis"))
selection, _ := common.NormalizeSelectionWithEllipsis(strings.TrimSpace(runtime.Str("selection-with-ellipsis")))
if selection != "" {
before := runtime.Bool("before")
// Redact the selection when logging — it is copied verbatim from
Expand Down
10 changes: 8 additions & 2 deletions shortcuts/doc/docs_update.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
args["markdown"] = v
}
if v := runtime.Str("selection-with-ellipsis"); v != "" {
args["selection_with_ellipsis"] = v
normalized, _ := common.NormalizeSelectionWithEllipsis(v)
args["selection_with_ellipsis"] = normalized

Check warning on line 82 in shortcuts/doc/docs_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update.go#L81-L82

Added lines #L81 - L82 were not covered by tests
}
if v := runtime.Str("selection-by-title"); v != "" {
args["selection_by_title"] = v
Expand Down Expand Up @@ -111,7 +112,12 @@
args["markdown"] = markdown
}
if v := runtime.Str("selection-with-ellipsis"); v != "" {
args["selection_with_ellipsis"] = v
normalized, changed := common.NormalizeSelectionWithEllipsis(v)
if changed {
fmt.Fprintf(runtime.IO().ErrOut,
"note: normalized --selection-with-ellipsis (curly quotes / CR line endings rewritten to canonical ASCII form for matching)\n")
}
args["selection_with_ellipsis"] = normalized

Check warning on line 120 in shortcuts/doc/docs_update.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/doc/docs_update.go#L115-L120

Added lines #L115 - L120 were not covered by tests
}
if v := runtime.Str("selection-by-title"); v != "" {
args["selection_by_title"] = v
Expand Down
9 changes: 7 additions & 2 deletions shortcuts/drive/drive_add_comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
}

// Doc/docx comment dry-run.
selection := runtime.Str("selection-with-ellipsis")
selection, _ := common.NormalizeSelectionWithEllipsis(runtime.Str("selection-with-ellipsis"))
mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID)

createPath := "/open-apis/drive/v1/files/:file_token/new_comments"
Expand Down Expand Up @@ -241,7 +241,12 @@
return executeSheetComment(runtime, docRef)
}

selection := runtime.Str("selection-with-ellipsis")
rawSelection := runtime.Str("selection-with-ellipsis")
selection, normalized := common.NormalizeSelectionWithEllipsis(rawSelection)
if normalized {
fmt.Fprintf(runtime.IO().ErrOut,
"note: normalized --selection-with-ellipsis (curly quotes / CR line endings rewritten to canonical ASCII form for matching)\n")
}

Check warning on line 249 in shortcuts/drive/drive_add_comment.go

View check run for this annotation

Codecov / codecov/patch

shortcuts/drive/drive_add_comment.go#L247-L249

Added lines #L247 - L249 were not covered by tests
blockID := strings.TrimSpace(runtime.Str("block-id"))
mode := resolveCommentMode(runtime.Bool("full-comment"), selection, blockID)

Expand Down
Loading