2021-01-17 21:46:25 +08:00
// Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
2018-04-13 15:19:31 +08:00
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
2019-02-02 16:18:25 +08:00
// You can obtain one at https://github.com/gogf/gf.
2018-04-13 15:19:31 +08:00
package ghttp
import (
2021-09-27 21:27:24 +08:00
"context"
2019-06-19 09:06:52 +08:00
"fmt"
2021-11-07 21:31:33 +08:00
"reflect"
2021-12-18 18:25:04 +08:00
"runtime"
2019-06-19 09:06:52 +08:00
"strings"
2019-07-29 21:01:19 +08:00
2021-10-11 21:41:56 +08:00
"github.com/gogf/gf/v2/container/glist"
2021-11-13 23:23:55 +08:00
"github.com/gogf/gf/v2/container/gtype"
"github.com/gogf/gf/v2/debug/gdebug"
"github.com/gogf/gf/v2/errors/gcode"
"github.com/gogf/gf/v2/errors/gerror"
2022-09-27 10:11:33 +08:00
"github.com/gogf/gf/v2/internal/consts"
2021-10-11 21:41:56 +08:00
"github.com/gogf/gf/v2/text/gregex"
"github.com/gogf/gf/v2/text/gstr"
2021-11-13 23:23:55 +08:00
"github.com/gogf/gf/v2/util/gmeta"
2022-11-07 17:51:37 +08:00
"github.com/gogf/gf/v2/util/gtag"
2018-04-13 15:19:31 +08:00
)
2020-03-04 22:52:56 +08:00
var (
// handlerIdGenerator is handler item id generator.
handlerIdGenerator = gtype . NewInt ( )
)
2022-03-19 17:58:21 +08:00
// routerMapKey creates and returns a unique router key for given parameters.
2020-03-05 16:08:55 +08:00
// This key is used for Server.routerMap attribute, which is mainly for checks for
// repeated router registering.
2023-07-06 21:29:33 +08:00
func ( s * Server ) routerMapKey ( hook HookName , method , path , domain string ) string {
return string ( hook ) + "%" + s . serveHandlerKey ( method , path , domain )
2020-03-04 17:29:23 +08:00
}
2019-09-29 14:27:09 +08:00
2020-03-04 17:29:23 +08:00
// parsePattern parses the given pattern to domain, method and path variable.
2019-06-19 09:06:52 +08:00
func ( s * Server ) parsePattern ( pattern string ) ( domain , method , path string , err error ) {
path = strings . TrimSpace ( pattern )
2021-09-19 10:01:09 +08:00
domain = DefaultDomainName
2020-12-14 13:26:48 +08:00
method = defaultMethod
2019-06-19 09:06:52 +08:00
if array , err := gregex . MatchString ( ` ([a-zA-Z]+):(.+) ` , pattern ) ; len ( array ) > 1 && err == nil {
path = strings . TrimSpace ( array [ 2 ] )
if v := strings . TrimSpace ( array [ 1 ] ) ; v != "" {
method = v
}
}
if array , err := gregex . MatchString ( ` (.+)@([\w\.\-]+) ` , path ) ; len ( array ) > 1 && err == nil {
path = strings . TrimSpace ( array [ 1 ] )
if v := strings . TrimSpace ( array [ 2 ] ) ; v != "" {
domain = v
}
}
if path == "" {
2021-08-24 21:18:59 +08:00
err = gerror . NewCode ( gcode . CodeInvalidParameter , "invalid pattern: URI should not be empty" )
2019-06-19 09:06:52 +08:00
}
if path != "/" {
path = strings . TrimRight ( path , "/" )
}
return
2018-04-13 15:19:31 +08:00
}
2021-11-07 21:31:33 +08:00
type setHandlerInput struct {
Prefix string
Pattern string
2022-05-06 20:25:21 +08:00
HandlerItem * HandlerItem
2021-11-07 21:31:33 +08:00
}
2022-03-19 17:58:21 +08:00
// setHandler creates router item with a given handler and pattern and registers the handler to the router tree.
// The router tree can be treated as a multilayer hash table, please refer to the comment in the following codes.
2020-03-04 17:29:23 +08:00
// This function is called during server starts up, which cares little about the performance. What really cares
2021-09-27 21:27:24 +08:00
// is the well-designed router storage structure for router searching when the request is under serving.
2021-11-07 21:31:33 +08:00
func ( s * Server ) setHandler ( ctx context . Context , in setHandlerInput ) {
var (
prefix = in . Prefix
pattern = in . Pattern
handler = in . HandlerItem
)
2021-12-18 18:25:04 +08:00
if handler . Name == "" {
handler . Name = runtime . FuncForPC ( handler . Info . Value . Pointer ( ) ) . Name ( )
}
2021-07-19 20:06:44 +08:00
if handler . Source == "" {
2022-09-27 10:11:33 +08:00
_ , file , line := gdebug . CallerWithFilter ( [ ] string { consts . StackFilterKeyForGoFrame } )
2021-07-19 20:06:44 +08:00
handler . Source = fmt . Sprintf ( ` %s:%d ` , file , line )
2020-03-17 14:48:52 +08:00
}
2019-06-19 09:06:52 +08:00
domain , method , uri , err := s . parsePattern ( pattern )
if err != nil {
2021-10-06 12:12:59 +08:00
s . Logger ( ) . Fatalf ( ctx , ` invalid pattern "%s", %+v ` , pattern , err )
2019-06-19 09:06:52 +08:00
return
}
2023-05-24 17:21:28 +08:00
// ====================================================================================
2022-03-19 17:58:21 +08:00
// Change the registered route according to meta info from its request structure.
2023-05-24 17:21:28 +08:00
// It supports multiple methods that are joined using char `,`.
// ====================================================================================
2021-11-07 21:31:33 +08:00
if handler . Info . Type != nil && handler . Info . Type . NumIn ( ) == 2 {
2022-02-24 22:07:27 +08:00
var objectReq = reflect . New ( handler . Info . Type . In ( 1 ) )
2022-11-07 17:51:37 +08:00
if v := gmeta . Get ( objectReq , gtag . Path ) ; ! v . IsEmpty ( ) {
2021-11-07 21:31:33 +08:00
uri = v . String ( )
}
2023-05-24 17:21:28 +08:00
if v := gmeta . Get ( objectReq , gtag . Domain ) ; ! v . IsEmpty ( ) {
domain = v . String ( )
}
2022-11-07 17:51:37 +08:00
if v := gmeta . Get ( objectReq , gtag . Method ) ; ! v . IsEmpty ( ) {
2021-11-07 21:31:33 +08:00
method = v . String ( )
}
2023-05-24 17:21:28 +08:00
// Multiple methods registering, which are joined using char `,`.
if gstr . Contains ( method , "," ) {
methods := gstr . SplitAndTrim ( method , "," )
for _ , v := range methods {
// Each method has it own handler.
clonedHandler := * handler
s . doSetHandler ( ctx , & clonedHandler , prefix , uri , pattern , v , domain )
}
return
}
// Converts `all` to `ALL`.
if gstr . Equal ( method , defaultMethod ) {
method = defaultMethod
2021-11-07 21:31:33 +08:00
}
}
2023-05-24 17:21:28 +08:00
s . doSetHandler ( ctx , handler , prefix , uri , pattern , method , domain )
}
2021-11-07 21:31:33 +08:00
2023-05-24 17:21:28 +08:00
func ( s * Server ) doSetHandler (
ctx context . Context , handler * HandlerItem ,
prefix , uri , pattern , method , domain string ,
) {
if ! s . isValidMethod ( method ) {
s . Logger ( ) . Fatalf (
ctx ,
` invalid method value "%s", should be in "%s" or "%s" ` ,
method , supportedHttpMethods , defaultMethod ,
)
}
2021-11-07 21:31:33 +08:00
// Prefix for URI feature.
if prefix != "" {
uri = prefix + "/" + strings . TrimLeft ( uri , "/" )
}
2022-02-17 22:24:50 +08:00
uri = strings . TrimRight ( uri , "/" )
if uri == "" {
uri = "/"
}
2021-11-07 21:31:33 +08:00
2019-06-19 09:06:52 +08:00
if len ( uri ) == 0 || uri [ 0 ] != '/' {
2021-10-06 12:12:59 +08:00
s . Logger ( ) . Fatalf ( ctx , ` invalid pattern "%s", URI should lead with '/' ` , pattern )
2019-06-19 09:06:52 +08:00
}
2020-03-04 17:29:23 +08:00
// Repeated router checks, this feature can be disabled by server configuration.
2022-03-29 20:31:00 +08:00
var routerKey = s . routerMapKey ( handler . HookName , method , uri , domain )
2019-12-18 19:37:07 +08:00
if ! s . config . RouteOverWrite {
2021-07-19 20:06:44 +08:00
switch handler . Type {
2021-10-06 14:22:58 +08:00
case HandlerTypeHandler , HandlerTypeObject :
2022-02-24 22:07:27 +08:00
if items , ok := s . routesMap [ routerKey ] ; ok {
2022-05-06 20:25:21 +08:00
var duplicatedHandler * HandlerItem
for i , item := range items {
switch item . Type {
2022-02-24 22:07:27 +08:00
case HandlerTypeHandler , HandlerTypeObject :
2022-05-06 20:25:21 +08:00
duplicatedHandler = items [ i ]
2023-09-07 20:22:20 +08:00
}
if duplicatedHandler != nil {
2022-02-24 22:07:27 +08:00
break
}
}
if duplicatedHandler != nil {
s . Logger ( ) . Fatalf (
ctx ,
2024-06-13 21:16:11 +08:00
"The duplicated route registry [%s] which is meaning [{hook}%%{method}:{path}@{domain}] at \n%s -> %s , which has already been registered at \n%s -> %s" +
"\nYou can disable duplicate route detection by modifying the server.routeOverWrite configuration, but this will cause some routes to be overwritten" ,
routerKey , handler . Source , handler . Name , duplicatedHandler . Source , duplicatedHandler . Name ,
2022-02-24 22:07:27 +08:00
)
}
2019-12-18 19:37:07 +08:00
}
2019-06-19 09:06:52 +08:00
}
}
2023-05-24 17:21:28 +08:00
// Unique id for each handler.
handler . Id = handlerIdGenerator . Add ( 1 )
2020-03-04 17:29:23 +08:00
// Create a new router by given parameter.
2021-07-19 20:06:44 +08:00
handler . Router = & Router {
2019-06-19 09:06:52 +08:00
Uri : uri ,
Domain : domain ,
2020-03-04 17:29:23 +08:00
Method : strings . ToUpper ( method ) ,
2019-06-19 09:06:52 +08:00
Priority : strings . Count ( uri [ 1 : ] , "/" ) ,
}
2021-07-19 20:06:44 +08:00
handler . Router . RegRule , handler . Router . RegNames = s . patternToRegular ( uri )
2018-07-27 19:03:32 +08:00
2019-08-03 15:54:12 +08:00
if _ , ok := s . serveTree [ domain ] ; ! ok {
s . serveTree [ domain ] = make ( map [ string ] interface { } )
2019-06-19 09:06:52 +08:00
}
2020-03-05 16:08:55 +08:00
// List array, very important for router registering.
2020-03-04 17:29:23 +08:00
// There may be multiple lists adding into this array when searching from root to leaf.
2022-03-29 20:31:00 +08:00
var (
array [ ] string
lists = make ( [ ] * glist . List , 0 )
)
2019-06-19 09:06:52 +08:00
if strings . EqualFold ( "/" , uri ) {
array = [ ] string { "/" }
} else {
array = strings . Split ( uri [ 1 : ] , "/" )
}
2020-03-04 17:29:23 +08:00
// Multilayer hash table:
// 1. Each node of the table is separated by URI path which is split by char '/'.
// 2. The key "*fuzz" specifies this node is a fuzzy node, which has no certain name.
// 3. The key "*list" is the list item of the node, MOST OF THE NODES HAVE THIS ITEM,
// especially the fuzzy node. NOTE THAT the fuzzy node must have the "*list" item,
// and the leaf node also has "*list" item. If the node is not a fuzzy node either
// a leaf, it neither has "*list" item.
// 2. The "*list" item is a list containing registered router items ordered by their
2023-10-16 20:41:10 +08:00
// priorities from high to low. If it's a fuzzy node, all the sub router items
// from this fuzzy node will also be added to its "*list" item.
2020-03-04 17:29:23 +08:00
// 3. There may be repeated router items in the router lists. The lists' priorities
// from root to leaf are from low to high.
2022-03-29 20:31:00 +08:00
var p = s . serveTree [ domain ]
2020-03-04 17:29:23 +08:00
for i , part := range array {
// Ignore empty URI part, like: /user//index
if part == "" {
continue
}
// Check if it's a fuzzy node.
if gregex . IsMatchString ( ` ^[:\*]|\ { [\w\.\-]+\}|\* ` , part ) {
part = "*fuzz"
// If it's a fuzzy node, it creates a "*list" item - which is a list - in the hash map.
// All the sub router items from this fuzzy node will also be added to its "*list" item.
if v , ok := p . ( map [ string ] interface { } ) [ "*list" ] ; ! ok {
newListForFuzzy := glist . New ( )
p . ( map [ string ] interface { } ) [ "*list" ] = newListForFuzzy
lists = append ( lists , newListForFuzzy )
} else {
lists = append ( lists , v . ( * glist . List ) )
}
}
2022-03-19 17:58:21 +08:00
// Make a new bucket for the current node.
2020-03-04 17:29:23 +08:00
if _ , ok := p . ( map [ string ] interface { } ) [ part ] ; ! ok {
p . ( map [ string ] interface { } ) [ part ] = make ( map [ string ] interface { } )
}
// Loop to next bucket.
p = p . ( map [ string ] interface { } ) [ part ]
// The leaf is a hash map and must have an item named "*list", which contains the router item.
// The leaf can be furthermore extended by adding more ket-value pairs into its map.
// Note that the `v != "*fuzz"` comparison is required as the list might be added in the former
// fuzzy checks.
if i == len ( array ) - 1 && part != "*fuzz" {
if v , ok := p . ( map [ string ] interface { } ) [ "*list" ] ; ! ok {
2020-03-04 22:52:56 +08:00
leafList := glist . New ( )
p . ( map [ string ] interface { } ) [ "*list" ] = leafList
lists = append ( lists , leafList )
2019-06-19 09:06:52 +08:00
} else {
2019-08-03 17:14:54 +08:00
lists = append ( lists , v . ( * glist . List ) )
2019-06-19 09:06:52 +08:00
}
}
}
2021-10-21 18:22:47 +08:00
// It iterates the list array of `lists`, compares priorities and inserts the new router item in
2020-03-04 17:29:23 +08:00
// the proper position of each list. The priority of the list is ordered from high to low.
2022-05-06 20:25:21 +08:00
var item * HandlerItem
2019-06-19 09:06:52 +08:00
for _ , l := range lists {
pushed := false
for e := l . Front ( ) ; e != nil ; e = e . Next ( ) {
2022-05-06 20:25:21 +08:00
item = e . Value . ( * HandlerItem )
2020-02-08 11:17:09 +08:00
// Checks the priority whether inserting the route item before current item,
2021-11-07 21:31:33 +08:00
// which means it has higher priority.
2019-09-29 14:27:09 +08:00
if s . compareRouterPriority ( handler , item ) {
2019-10-01 16:35:44 +08:00
l . InsertBefore ( e , handler )
2019-09-29 14:27:09 +08:00
pushed = true
goto end
2019-06-19 09:06:52 +08:00
}
}
2019-08-06 23:10:37 +08:00
end :
2020-02-08 11:17:09 +08:00
// Just push back in default.
2019-06-19 09:06:52 +08:00
if ! pushed {
l . PushBack ( handler )
}
}
2020-02-08 11:17:09 +08:00
// Initialize the route map item.
2020-03-04 22:52:56 +08:00
if _ , ok := s . routesMap [ routerKey ] ; ! ok {
2022-05-06 20:25:21 +08:00
s . routesMap [ routerKey ] = make ( [ ] * HandlerItem , 0 )
2019-06-19 09:06:52 +08:00
}
2020-03-17 14:48:52 +08:00
2022-11-14 19:57:39 +08:00
// Append the route.
s . routesMap [ routerKey ] = append ( s . routesMap [ routerKey ] , handler )
2018-04-13 15:19:31 +08:00
}
2023-05-24 17:21:28 +08:00
func ( s * Server ) isValidMethod ( method string ) bool {
if gstr . Equal ( method , defaultMethod ) {
return true
}
_ , ok := methodsMap [ strings . ToUpper ( method ) ]
return ok
}
2021-10-21 18:22:47 +08:00
// compareRouterPriority compares the priority between `newItem` and `oldItem`. It returns true
// if `newItem`'s priority is higher than `oldItem`, else it returns false. The higher priority
2021-11-07 21:31:33 +08:00
// item will be inserted into the router list before the other one.
2020-03-04 23:32:27 +08:00
//
// Comparison rules:
// 1. The middleware has the most high priority.
2021-11-07 21:31:33 +08:00
// 2. URI: The deeper, the higher (simply check the count of char '/' in the URI).
2020-03-04 23:32:27 +08:00
// 3. Route type: {xxx} > :xxx > *xxx.
2022-05-06 20:25:21 +08:00
func ( s * Server ) compareRouterPriority ( newItem * HandlerItem , oldItem * HandlerItem ) bool {
2022-03-19 17:58:21 +08:00
// If they're all types of middleware, the priority is according to their registered sequence.
2021-10-06 14:22:58 +08:00
if newItem . Type == HandlerTypeMiddleware && oldItem . Type == HandlerTypeMiddleware {
2019-08-03 18:08:10 +08:00
return false
}
2020-03-04 23:32:27 +08:00
// The middleware has the most high priority.
2021-10-06 14:22:58 +08:00
if newItem . Type == HandlerTypeMiddleware && oldItem . Type != HandlerTypeMiddleware {
2019-08-03 18:08:10 +08:00
return true
}
2021-11-07 21:31:33 +08:00
// URI: The deeper, the higher (simply check the count of char '/' in the URI).
2021-07-19 20:06:44 +08:00
if newItem . Router . Priority > oldItem . Router . Priority {
2019-06-19 09:06:52 +08:00
return true
}
2021-07-19 20:06:44 +08:00
if newItem . Router . Priority < oldItem . Router . Priority {
2019-06-19 09:06:52 +08:00
return false
}
2020-06-03 00:09:51 +08:00
// Compare the length of their URI,
// but the fuzzy and named parts of the URI are not calculated to the result.
2021-07-13 23:01:31 +08:00
// Example:
2020-06-03 00:09:51 +08:00
// /admin-goods-{page} > /admin-{page}
// /{hash}.{type} > /{hash}
var uriNew , uriOld string
2021-07-19 20:06:44 +08:00
uriNew , _ = gregex . ReplaceString ( ` \ { [^/]+?\} ` , "" , newItem . Router . Uri )
uriOld , _ = gregex . ReplaceString ( ` \ { [^/]+?\} ` , "" , oldItem . Router . Uri )
2020-06-03 00:09:51 +08:00
uriNew , _ = gregex . ReplaceString ( ` :[^/]+? ` , "" , uriNew )
uriOld , _ = gregex . ReplaceString ( ` :[^/]+? ` , "" , uriOld )
uriNew , _ = gregex . ReplaceString ( ` \*[^/]* ` , "" , uriNew ) // Replace "/*" and "/*any".
uriOld , _ = gregex . ReplaceString ( ` \*[^/]* ` , "" , uriOld ) // Replace "/*" and "/*any".
if len ( uriNew ) > len ( uriOld ) {
return true
}
if len ( uriNew ) < len ( uriOld ) {
return false
}
// Route type checks: {xxx} > :xxx > *xxx.
2021-07-13 23:01:31 +08:00
// Example:
2020-06-03 00:09:51 +08:00
// /name/act > /{name}/:act
var (
fuzzyCountFieldNew int
fuzzyCountFieldOld int
fuzzyCountNameNew int
fuzzyCountNameOld int
fuzzyCountAnyNew int
fuzzyCountAnyOld int
fuzzyCountTotalNew int
fuzzyCountTotalOld int
)
2021-07-19 20:06:44 +08:00
for _ , v := range newItem . Router . Uri {
2019-06-19 09:06:52 +08:00
switch v {
case '{' :
fuzzyCountFieldNew ++
case ':' :
fuzzyCountNameNew ++
case '*' :
fuzzyCountAnyNew ++
}
}
2021-07-19 20:06:44 +08:00
for _ , v := range oldItem . Router . Uri {
2019-06-19 09:06:52 +08:00
switch v {
case '{' :
fuzzyCountFieldOld ++
case ':' :
fuzzyCountNameOld ++
case '*' :
fuzzyCountAnyOld ++
}
}
fuzzyCountTotalNew = fuzzyCountFieldNew + fuzzyCountNameNew + fuzzyCountAnyNew
fuzzyCountTotalOld = fuzzyCountFieldOld + fuzzyCountNameOld + fuzzyCountAnyOld
if fuzzyCountTotalNew < fuzzyCountTotalOld {
return true
}
if fuzzyCountTotalNew > fuzzyCountTotalOld {
return false
}
2018-08-19 11:25:15 +08:00
2022-03-19 17:58:21 +08:00
// If the counts of their fuzzy rules are equal.
2018-08-19 11:25:15 +08:00
2020-03-04 23:32:27 +08:00
// Eg: /name/{act} > /name/:act
2019-06-19 09:06:52 +08:00
if fuzzyCountFieldNew > fuzzyCountFieldOld {
return true
}
if fuzzyCountFieldNew < fuzzyCountFieldOld {
return false
}
2020-03-04 23:32:27 +08:00
// Eg: /name/:act > /name/*act
2019-06-19 09:06:52 +08:00
if fuzzyCountNameNew > fuzzyCountNameOld {
return true
}
if fuzzyCountNameNew < fuzzyCountNameOld {
return false
}
2018-11-19 23:13:12 +08:00
2020-03-04 23:32:27 +08:00
// It then compares the accuracy of their http method,
// the more accurate the more priority.
2021-07-19 20:06:44 +08:00
if newItem . Router . Method != defaultMethod {
2019-06-19 09:06:52 +08:00
return true
}
2021-07-19 20:06:44 +08:00
if oldItem . Router . Method != defaultMethod {
2019-06-19 09:06:52 +08:00
return true
}
2018-11-19 23:13:12 +08:00
2020-03-04 23:32:27 +08:00
// If they have different router type,
// the new router item has more priority than the other one.
2021-10-06 14:22:58 +08:00
if newItem . Type == HandlerTypeHandler || newItem . Type == HandlerTypeObject {
2019-12-19 15:38:34 +08:00
return true
}
2020-03-04 23:32:27 +08:00
// Other situations, like HOOK items,
// the old router item has more priority than the other one.
2019-12-19 15:38:34 +08:00
return false
2018-04-13 15:19:31 +08:00
}
2022-03-19 17:58:21 +08:00
// patternToRegular converts route rule to according to regular expression.
2020-03-04 23:32:27 +08:00
func ( s * Server ) patternToRegular ( rule string ) ( regular string , names [ ] string ) {
2019-06-19 09:06:52 +08:00
if len ( rule ) < 2 {
return rule , nil
}
2020-03-04 23:32:27 +08:00
regular = "^"
2022-03-29 20:31:00 +08:00
var array = strings . Split ( rule [ 1 : ] , "/" )
2019-06-19 09:06:52 +08:00
for _ , v := range array {
if len ( v ) == 0 {
continue
}
switch v [ 0 ] {
case ':' :
if len ( v ) > 1 {
2020-03-04 23:32:27 +08:00
regular += ` /([^/]+) `
2019-06-19 09:06:52 +08:00
names = append ( names , v [ 1 : ] )
} else {
2020-03-04 23:32:27 +08:00
regular += ` /[^/]+ `
2019-06-19 09:06:52 +08:00
}
case '*' :
if len ( v ) > 1 {
2020-03-04 23:32:27 +08:00
regular += ` / { 0,1}(.*) `
2019-06-19 09:06:52 +08:00
names = append ( names , v [ 1 : ] )
} else {
2020-03-04 23:32:27 +08:00
regular += ` / { 0,1}.* `
2019-06-19 09:06:52 +08:00
}
default :
2020-03-04 17:29:23 +08:00
// Special chars replacement.
2019-06-19 09:06:52 +08:00
v = gstr . ReplaceByMap ( v , map [ string ] string {
` . ` : ` \. ` ,
` + ` : ` \+ ` ,
` * ` : ` .* ` ,
} )
s , _ := gregex . ReplaceStringFunc ( ` \ { [\w\.\-]+\} ` , v , func ( s string ) string {
names = append ( names , s [ 1 : len ( s ) - 1 ] )
return ` ([^/]+) `
} )
if strings . EqualFold ( s , v ) {
2020-03-04 23:32:27 +08:00
regular += "/" + v
2019-06-19 09:06:52 +08:00
} else {
2020-03-04 23:32:27 +08:00
regular += "/" + s
2019-06-19 09:06:52 +08:00
}
}
}
2020-03-04 23:32:27 +08:00
regular += ` $ `
2019-06-19 09:06:52 +08:00
return
2018-04-13 15:19:31 +08:00
}