Repository: zeromicro/go-zero (go)

Issue:
stringx.NewReplacer error
**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior, if applicable:

1. The code is
- https://go.dev/play/p/yOH2YreNQUg

   ```go
   package main

   import (
	"fmt"
	"github.com/zeromicro/go-zero/core/stringx"
   )

   func main() {
	replacer := stringx.NewReplacer(map[string]string{
		"日本":    "法国",
		"日本的首都": "东京",
		"东京":    "日本的首都",
	})
	fmt.Println(replacer.Replace("日本的首都是东京"))
   }
   ```

3. The error is

   [Example document output](https://go-zero.dev/cn/docs/blog/tool/keywords)
   ```
   东京是日本的首都
   ```

   test output
   ```
   法国的首都是日本的首都
   ```

Pull request:
fix #2835， replace shoud replace the longest match
In the `Replace` method,  the replacer replace the shortest first match word, not the longest word said [in the doc](https://go-zero.dev/docs/blog/tool/keywords).

Patch:
diff --git a/core/stringx/node.go b/core/stringx/node.go
index 867afd94..4b6fb6e1 100644
--- a/core/stringx/node.go
+++ b/core/stringx/node.go
@@ -98,3 +98,45 @@ func (n *node) find(chars []rune) []scope {
 
 	return scopes
 }
+
+func (n *node) longestMatch(chars []rune, start int) (used int, jump *node, matched bool) {
+	cur := n
+	var matchedNode *node
+	for i := start; i < len(chars); i++ {
+		child, ok := cur.children[chars[i]]
+		if ok {
+			cur = child
+			if cur.end {
+				matchedNode = cur
+			}
+		} else {
+			if matchedNode != nil {
+				return matchedNode.depth, nil, true
+			}
+			if n.end {
+				return start, nil, true
+			}
+			var jump *node
+			for cur.fail != nil {
+				jump, ok = cur.fail.children[chars[i]]
+				if ok {
+					break
+				}
+				cur = cur.fail
+			}
+			if jump != nil {
+				return i + 1 - jump.depth, jump, false
+			}
+			return i + 1, nil, false
+		}
+	}
+	// this longest matched node
+	if matchedNode != nil {
+		return matchedNode.depth, nil, true
+	}
+	// last mathed node
+	if n.end {
+		return start, nil, true
+	}
+	return len(chars), nil, false
+}
diff --git a/core/stringx/replacer.go b/core/stringx/replacer.go
index 947ae69a..3edcf9c2 100644
--- a/core/stringx/replacer.go
+++ b/core/stringx/replacer.go
@@ -30,68 +30,28 @@ func NewReplacer(mapping map[string]string) Replacer {
 
 // Replace replaces text with given substitutes.
 func (r *replacer) Replace(text string) string {
-	var builder strings.Builder
-	var start int
-	chars := []rune(text)
-	size := len(chars)
-
-	for start < size {
-		cur := r.node
-
-		if start > 0 {
-			builder.WriteString(string(chars[:start]))
-		}
-
-		for i := start; i < size; i++ {
-			child, ok := cur.children[chars[i]]
-			if ok {
-				cur = child
-			} else if cur == r.node {
-				builder.WriteRune(chars[i])
-				// cur already points to root, set start only
-				start = i + 1
-				continue
+	var buf strings.Builder
+	target := []rune(text)
+	cur := r.node
+	nextStart := 0
+	for len(target) != 0 {
+		used, jump, matched := cur.longestMatch(target, nextStart)
+		if matched {
+			replaced := r.mapping[string(target[:used])]
+			target = append([]rune(replaced), target[used:]...)
+			cur = r.node
+			nextStart = 0
+		} else {
+			buf.WriteString(string(target[:used]))
+			target = target[used:]
+			if jump != nil {
+				cur = jump
+				nextStart = jump.depth
 			} else {
-				curDepth := cur.depth
-				cur = cur.fail
-				child, ok = cur.children[chars[i]]
-				if !ok {
-					// write this path
-					builder.WriteString(string(chars[i-curDepth : i+1]))
-					// go to root
-					cur = r.node
-					start = i + 1
-					continue
-				}
-
-				failDepth := cur.depth
-				// write path before jump
-				builder.WriteString(string(chars[start : start+curDepth-failDepth]))
-				start += curDepth - failDepth
-				cur = child
-			}
-
-			if cur.end {
-				val := string(chars[i+1-cur.depth : i+1])
-				builder.WriteString(r.mapping[val])
-				builder.WriteString(string(chars[i+1:]))
-				// only matching this path, all previous paths are done
-				if start >= i+1-cur.depth && i+1 >= size {
-					return builder.String()
-				}
-
-				chars = []rune(builder.String())
-				size = len(chars)
-				builder.Reset()
-				break
+				cur = r.node
+				nextStart = 0
 			}
 		}
-
-		if !cur.end {
-			builder.WriteString(string(chars[start:]))
-			return builder.String()
-		}
 	}
-
-	return string(chars)
+	return buf.String()
 }
diff --git a/go.mod b/go.mod
index f02a63ea..ccf2d212 100644
--- a/go.mod
+++ b/go.mod
@@ -25,13 +25,13 @@ require (
 	go.etcd.io/etcd/api/v3 v3.5.7
 	go.etcd.io/etcd/client/v3 v3.5.7
 	go.mongodb.org/mongo-driver v1.11.1
-	go.opentelemetry.io/otel v1.12.0
-	go.opentelemetry.io/otel/exporters/jaeger v1.12.0
+	go.opentelemetry.io/otel v1.13.0
+	go.opentelemetry.io/otel/exporters/jaeger v1.13.0
 	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.2
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0
-	go.opentelemetry.io/otel/exporters/zipkin v1.12.0
-	go.opentelemetry.io/otel/sdk v1.12.0
-	go.opentelemetry.io/otel/trace v1.12.0
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0
+	go.opentelemetry.io/otel/exporters/zipkin v1.13.0
+	go.opentelemetry.io/otel/sdk v1.13.0
+	go.opentelemetry.io/otel/trace v1.13.0
 	go.uber.org/automaxprocs v1.5.1
 	go.uber.org/goleak v1.2.0
 	golang.org/x/sys v0.5.0
@@ -104,8 +104,8 @@ require (
 	github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
 	github.com/yuin/gopher-lua v0.0.0-20220504180219-658193537a64 // indirect
 	go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0 // indirect
-	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 // indirect
 	go.opentelemetry.io/proto/otlp v0.19.0 // indirect
 	go.uber.org/atomic v1.10.0 // indirect
 	go.uber.org/multierr v1.9.0 // indirect
diff --git a/go.sum b/go.sum
index a4c80d1c..f143e6b3 100644
--- a/go.sum
+++ b/go.sum
@@ -406,24 +406,24 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.opentelemetry.io/otel v1.12.0 h1:IgfC7kqQrRccIKuB7Cl+SRUmsKbEwSGPr0Eu+/ht1SQ=
-go.opentelemetry.io/otel v1.12.0/go.mod h1:geaoz0L0r1BEOR81k7/n9W4TCXYCJ7bPO7K374jQHG0=
-go.opentelemetry.io/otel/exporters/jaeger v1.12.0 h1:1Vy11S0iAD70EPfcP3N2f2IhLq/cIuTW+Zt010MswR8=
-go.opentelemetry.io/otel/exporters/jaeger v1.12.0/go.mod h1:SCLbaspEoU9mGJZB6ksc2iSGU6CLWY5yefchDqOM0IM=
-go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0 h1:UfDENi+LTcLjQ/JhaXimjlIgn7wWjwbEMmdREm2Gyng=
-go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0/go.mod h1:rqbht/LlhVBgn5+k3M5QK96K5Xb0DvXpMJ5SFQpY6uw=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0 h1:ZVqtSAxrR4+ofzayuww0/EKamCjjnwnXTMRZzMudJoU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0/go.mod h1:IlaGLENJkAl9+Xoo3J0unkdOwtL+rmqZ3ryMjUtYA94=
+go.opentelemetry.io/otel v1.13.0 h1:1ZAKnNQKwBBxFtww/GwxNUyTf0AxkZzrukO8MeXqe4Y=
+go.opentelemetry.io/otel v1.13.0/go.mod h1:FH3RtdZCzRkJYFTCsAKDy9l/XYjMdNv6QrkFFB8DvVg=
+go.opentelemetry.io/otel/exporters/jaeger v1.13.0 h1:VAMoGujbVV8Q0JNM/cEbhzUIWWBxnEqH45HP9iBKN04=
+go.opentelemetry.io/otel/exporters/jaeger v1.13.0/go.mod h1:fHwbmle6mBFJA1p2ZIhilvffCdq/dM5UTIiCOmEjS+w=
+go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0 h1:pa05sNT/P8OsIQ8mPZKTIyiBuzS/xDGLVx+DCt0y6Vs=
+go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.13.0/go.mod h1:rqbht/LlhVBgn5+k3M5QK96K5Xb0DvXpMJ5SFQpY6uw=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0 h1:Any/nVxaoMq1T2w0W85d6w5COlLuCCgOYKQhJJWEMwQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.13.0/go.mod h1:46vAP6RWfNn7EKov73l5KBFlNxz8kYlxR1woU+bJ4ZY=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.2 h1:ERwKPn9Aer7Gxsc0+ZlutlH1bEEAUXAUhqm3Y45ABbk=
 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.11.2/go.mod h1:jWZUM2MWhWCJ9J9xVbRx7tzK1mXKpAlze4CeulycwVY=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0 h1:L23MzcHDznr05xOM1Ng1F98L0nVd7hm/S7y2jW9IRB4=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0/go.mod h1:C+onYX2j5QH653b3wGJwowYr8jLMjBJw35QcaCQQK0U=
-go.opentelemetry.io/otel/exporters/zipkin v1.12.0 h1:0Pvqi4MAvQ28rGUFoH8uIaNKNCGVLNpn7TbRCkB1gP8=
-go.opentelemetry.io/otel/exporters/zipkin v1.12.0/go.mod h1:zrxX/glbLVD/EFi01JCn2W1KdWNrJb3FkBHF+oRPlXs=
-go.opentelemetry.io/otel/sdk v1.12.0 h1:8npliVYV7qc0t1FKdpU08eMnOjgPFMnriPhn0HH4q3o=
-go.opentelemetry.io/otel/sdk v1.12.0/go.mod h1:WYcvtgquYvgODEvxOry5owO2y9MyciW7JqMz6cpXShE=
-go.opentelemetry.io/otel/trace v1.12.0 h1:p28in++7Kd0r2d8gSt931O57fdjUyWxkVbESuILAeUc=
-go.opentelemetry.io/otel/trace v1.12.0/go.mod h1:pHlgBynn6s25qJ2szD+Bv+iwKJttjHSI3lUAyf0GNuQ=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0 h1:Ntu7izEOIRHEgQNjbGc7j3eNtYMAiZfElJJ4JiiRDH4=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.13.0/go.mod h1:wZ9SAjm2sjw3vStBhlCfMZWZusyOQrwrHOFo00jyMC4=
+go.opentelemetry.io/otel/exporters/zipkin v1.13.0 h1:RqPV1VhJjrx28qOKYFPj3Mso56uaBovur3GZehF9y9s=
+go.opentelemetry.io/otel/exporters/zipkin v1.13.0/go.mod h1:x6S2VkXmdpoYUqQx9FKiMEsndal6xkcwDdV0Oi1RlLM=
+go.opentelemetry.io/otel/sdk v1.13.0 h1:BHib5g8MvdqS65yo2vV1s6Le42Hm6rrw08qU6yz5JaM=
+go.opentelemetry.io/otel/sdk v1.13.0/go.mod h1:YLKPx5+6Vx/o1TCUYYs+bpymtkmazOMT6zoRrC7AQ7I=
+go.opentelemetry.io/otel/trace v1.13.0 h1:CBgRZ6ntv+Amuj1jDsMhZtlAPT6gbyIRdaIzFhfBSdY=
+go.opentelemetry.io/otel/trace v1.13.0/go.mod h1:muCvmmO9KKpvuXSf3KKAXXB2ygNYHQ+ZfI5X08d3tds=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw=
 go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U=


Test patch:
diff --git a/core/stringx/replacer_test.go b/core/stringx/replacer_test.go
index 8cb8851b..0d3c4015 100644
--- a/core/stringx/replacer_test.go
+++ b/core/stringx/replacer_test.go
@@ -51,6 +51,15 @@ func TestReplacer_ReplaceMultiMatches(t *testing.T) {
 	assert.Equal(t, "零一23四五一23四五", NewReplacer(mapping).Replace("零一二三四五一二三四五"))
 }
 
+func TestReplacer_ReplaceLongestMatching(t *testing.T) {
+	keywords := map[string]string{
+		"日本":    "japan",
+		"日本的首都": "东京",
+	}
+	replacer := NewReplacer(keywords)
+	assert.Equal(t, "东京在japan", replacer.Replace("日本的首都在日本"))
+}
+
 func TestReplacer_ReplaceJumpToFail(t *testing.T) {
 	mapping := map[string]string{
 		"bcdf": "1235",