diff --git a/pkg/prosemirror/node.go b/pkg/prosemirror/node.go index 3f84501a5..932221dff 100644 --- a/pkg/prosemirror/node.go +++ b/pkg/prosemirror/node.go @@ -17,6 +17,7 @@ package prosemirror import ( "encoding/json" "fmt" + "unicode/utf8" ) type ( @@ -168,13 +169,12 @@ func (n Node) TableCellAttrs() (TableCellAttrs, error) { return a, nil } -// TextLength returns the total length of all text content in the node tree, -// measured in bytes (consistent with Go's len on strings). Only text carried -// by leaf text nodes is counted; structural markup is excluded. +// TextLength returns the total number of characters (Unicode code points) +// across all text nodes in the tree. Structural markup is excluded. func (n Node) TextLength() int { length := 0 if n.Text != nil { - length += len(*n.Text) + length += utf8.RuneCountInString(*n.Text) } for _, child := range n.Content { length += child.TextLength() diff --git a/pkg/prosemirror/node_test.go b/pkg/prosemirror/node_test.go index ff3676430..d1a605334 100644 --- a/pkg/prosemirror/node_test.go +++ b/pkg/prosemirror/node_test.go @@ -487,6 +487,17 @@ func TestTextLength(t *testing.T) { }, ) + t.Run( + "multi-byte unicode characters", + func(t *testing.T) { + t.Parallel() + raw := `{"type":"doc","content":[{"type":"paragraph","content":[{"type":"text","text":"café résumé"}]}]}` + doc, err := Parse(raw) + require.NoError(t, err) + assert.Equal(t, 11, doc.TextLength()) + }, + ) + t.Run( "testdata document", func(t *testing.T) {