@@ -42,6 +42,7 @@ func NewFuncMap() template.FuncMap {
4242 "HTMLFormat" : htmlutil .HTMLFormat ,
4343 "HTMLEscape" : htmlEscape ,
4444 "QueryEscape" : queryEscape ,
45+ "QueryBuild" : QueryBuild ,
4546 "JSEscape" : jsEscapeSafe ,
4647 "SanitizeHTML" : SanitizeHTML ,
4748 "URLJoin" : util .URLJoin ,
@@ -293,6 +294,72 @@ func timeEstimateString(timeSec any) string {
293294 return util .TimeEstimateString (v )
294295}
295296
297+ // QueryBuild builds a query string from a list of key-value pairs.
298+ // It omits the nil and empty strings, but it doesn't omit other zero values,
299+ // because the zero value of number types may have a meaning.
300+ func QueryBuild (a ... any ) template.URL {
301+ var s string
302+ if len (a )% 2 == 1 {
303+ if v , ok := a [0 ].(string ); ok {
304+ if v == "" || (v [0 ] != '?' && v [0 ] != '&' ) {
305+ panic ("QueryBuild: invalid argument" )
306+ }
307+ s = v
308+ } else if v , ok := a [0 ].(template.URL ); ok {
309+ s = string (v )
310+ } else {
311+ panic ("QueryBuild: invalid argument" )
312+ }
313+ }
314+ for i := len (a ) % 2 ; i < len (a ); i += 2 {
315+ k , ok := a [i ].(string )
316+ if ! ok {
317+ panic ("QueryBuild: invalid argument" )
318+ }
319+ var v string
320+ if va , ok := a [i + 1 ].(string ); ok {
321+ v = va
322+ } else if a [i + 1 ] != nil {
323+ v = fmt .Sprint (a [i + 1 ])
324+ }
325+ // pos1 to pos2 is the "k=v&" part, "&" is optional
326+ pos1 := strings .Index (s , "&" + k + "=" )
327+ if pos1 != - 1 {
328+ pos1 ++
329+ } else {
330+ pos1 = strings .Index (s , "?" + k + "=" )
331+ if pos1 != - 1 {
332+ pos1 ++
333+ } else if strings .HasPrefix (s , k + "=" ) {
334+ pos1 = 0
335+ }
336+ }
337+ pos2 := len (s )
338+ if pos1 == - 1 {
339+ pos1 = len (s )
340+ } else {
341+ pos2 = pos1 + 1
342+ for pos2 < len (s ) && s [pos2 - 1 ] != '&' {
343+ pos2 ++
344+ }
345+ }
346+ if v != "" {
347+ sep := ""
348+ hasPrefixSep := pos1 == 0 || (pos1 <= len (s ) && (s [pos1 - 1 ] == '?' || s [pos1 - 1 ] == '&' ))
349+ if ! hasPrefixSep {
350+ sep = "&"
351+ }
352+ s = s [:pos1 ] + sep + k + "=" + url .QueryEscape (v ) + "&" + s [pos2 :]
353+ } else {
354+ s = s [:pos1 ] + s [pos2 :]
355+ }
356+ }
357+ if s != "" && s != "&" && s [len (s )- 1 ] == '&' {
358+ s = s [:len (s )- 1 ]
359+ }
360+ return template .URL (s )
361+ }
362+
296363func panicIfDevOrTesting () {
297364 if ! setting .IsProd || setting .IsInTesting {
298365 panic ("legacy template functions are for backward compatibility only, do not use them in new code" )
0 commit comments