4
4
package templates
5
5
6
6
import (
7
+ "bufio"
7
8
"bytes"
8
9
"context"
9
10
"errors"
@@ -18,19 +19,13 @@ import (
18
19
"sync/atomic"
19
20
texttemplate "text/template"
20
21
22
+ "code.gitea.io/gitea/modules/assetfs"
21
23
"code.gitea.io/gitea/modules/log"
22
24
"code.gitea.io/gitea/modules/setting"
23
25
"code.gitea.io/gitea/modules/util"
24
26
)
25
27
26
- var (
27
- rendererKey interface {} = "templatesHtmlRenderer"
28
-
29
- templateError = regexp .MustCompile (`^template: (.*):([0-9]+): (.*)` )
30
- notDefinedError = regexp .MustCompile (`^template: (.*):([0-9]+): function "(.*)" not defined` )
31
- unexpectedError = regexp .MustCompile (`^template: (.*):([0-9]+): unexpected "(.*)" in operand` )
32
- expectedEndError = regexp .MustCompile (`^template: (.*):([0-9]+): expected end; found (.*)` )
33
- )
28
+ var rendererKey interface {} = "templatesHtmlRenderer"
34
29
35
30
type HTMLRender struct {
36
31
templates atomic.Pointer [template.Template ]
@@ -107,11 +102,12 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
107
102
108
103
renderer := & HTMLRender {}
109
104
if err := renderer .CompileTemplates (); err != nil {
110
- wrapFatal (handleNotDefinedPanicError (err ))
111
- wrapFatal (handleUnexpected (err ))
112
- wrapFatal (handleExpectedEnd (err ))
113
- wrapFatal (handleGenericTemplateError (err ))
114
- log .Fatal ("HTMLRenderer error: %v" , err )
105
+ p := & templateErrorPrettier {assets : AssetFS ()}
106
+ wrapFatal (p .handleFuncNotDefinedError (err ))
107
+ wrapFatal (p .handleUnexpectedOperandError (err ))
108
+ wrapFatal (p .handleExpectedEndError (err ))
109
+ wrapFatal (p .handleGenericTemplateError (err ))
110
+ log .Fatal ("HTMLRenderer CompileTemplates error: %v" , err )
115
111
}
116
112
if ! setting .IsProd {
117
113
go AssetFS ().WatchLocalChanges (ctx , func () {
@@ -123,148 +119,153 @@ func HTMLRenderer(ctx context.Context) (context.Context, *HTMLRender) {
123
119
return context .WithValue (ctx , rendererKey , renderer ), renderer
124
120
}
125
121
126
- func wrapFatal (format string , args [] interface {} ) {
127
- if format == "" {
122
+ func wrapFatal (msg string ) {
123
+ if msg == "" {
128
124
return
129
125
}
130
- log .FatalWithSkip (1 , format , args ... )
126
+ log .FatalWithSkip (1 , "Unable to compile templates, %s" , msg )
131
127
}
132
128
133
- func handleGenericTemplateError (err error ) (string , []interface {}) {
134
- groups := templateError .FindStringSubmatch (err .Error ())
135
- if len (groups ) != 4 {
136
- return "" , nil
137
- }
138
-
139
- templateName , lineNumberStr , message := groups [1 ], groups [2 ], groups [3 ]
140
- filename := fmt .Sprintf ("%s (provided by %s)" , templateName , AssetFS ().GetFileLayerName (templateName + ".tmpl" ))
141
- lineNumber , _ := strconv .Atoi (lineNumberStr )
142
- line := GetLineFromTemplate (templateName , lineNumber , "" , - 1 )
143
-
144
- return "PANIC: Unable to compile templates!\n %s in template file %s at line %d:\n \n %s\n Stacktrace:\n \n %s" , []interface {}{message , filename , lineNumber , log .NewColoredValue (line , log .Reset ), log .Stack (2 )}
129
+ type templateErrorPrettier struct {
130
+ assets * assetfs.LayeredFS
145
131
}
146
132
147
- func handleNotDefinedPanicError (err error ) (string , []interface {}) {
148
- groups := notDefinedError .FindStringSubmatch (err .Error ())
149
- if len (groups ) != 4 {
150
- return "" , nil
151
- }
152
-
153
- templateName , lineNumberStr , functionName := groups [1 ], groups [2 ], groups [3 ]
154
- functionName , _ = strconv .Unquote (`"` + functionName + `"` )
155
- filename := fmt .Sprintf ("%s (provided by %s)" , templateName , AssetFS ().GetFileLayerName (templateName + ".tmpl" ))
156
- lineNumber , _ := strconv .Atoi (lineNumberStr )
157
- line := GetLineFromTemplate (templateName , lineNumber , functionName , - 1 )
133
+ var reGenericTemplateError = regexp .MustCompile (`^template: (.*):([0-9]+): (.*)` )
158
134
159
- return "PANIC: Unable to compile templates!\n Undefined function %q in template file %s at line %d:\n \n %s" , []interface {}{functionName , filename , lineNumber , log .NewColoredValue (line , log .Reset )}
160
- }
161
-
162
- func handleUnexpected (err error ) (string , []interface {}) {
163
- groups := unexpectedError .FindStringSubmatch (err .Error ())
135
+ func (p * templateErrorPrettier ) handleGenericTemplateError (err error ) string {
136
+ groups := reGenericTemplateError .FindStringSubmatch (err .Error ())
164
137
if len (groups ) != 4 {
165
- return "" , nil
138
+ return ""
166
139
}
167
-
168
- templateName , lineNumberStr , unexpected := groups [1 ], groups [2 ], groups [3 ]
169
- unexpected , _ = strconv .Unquote (`"` + unexpected + `"` )
170
- filename := fmt .Sprintf ("%s (provided by %s)" , templateName , AssetFS ().GetFileLayerName (templateName + ".tmpl" ))
171
- lineNumber , _ := strconv .Atoi (lineNumberStr )
172
- line := GetLineFromTemplate (templateName , lineNumber , unexpected , - 1 )
173
-
174
- return "PANIC: Unable to compile templates!\n Unexpected %q in template file %s at line %d:\n \n %s" , []interface {}{unexpected , filename , lineNumber , log .NewColoredValue (line , log .Reset )}
140
+ tmplName , lineStr , message := groups [1 ], groups [2 ], groups [3 ]
141
+ return p .makeDetailedError (message , tmplName , lineStr , - 1 , "" )
175
142
}
176
143
177
- func handleExpectedEnd (err error ) (string , []interface {}) {
178
- groups := expectedEndError .FindStringSubmatch (err .Error ())
179
- if len (groups ) != 4 {
180
- return "" , nil
181
- }
182
-
183
- templateName , lineNumberStr , unexpected := groups [1 ], groups [2 ], groups [3 ]
184
- filename := fmt .Sprintf ("%s (provided by %s)" , templateName , AssetFS ().GetFileLayerName (templateName + ".tmpl" ))
185
- lineNumber , _ := strconv .Atoi (lineNumberStr )
186
- line := GetLineFromTemplate (templateName , lineNumber , unexpected , - 1 )
144
+ var reFuncNotDefinedError = regexp .MustCompile (`^template: (.*):([0-9]+): (function "(.*)" not defined)` )
187
145
188
- return "PANIC: Unable to compile templates!\n Missing end with unexpected %q in template file %s at line %d:\n \n %s" , []interface {}{unexpected , filename , lineNumber , log .NewColoredValue (line , log .Reset )}
146
+ func (p * templateErrorPrettier ) handleFuncNotDefinedError (err error ) string {
147
+ groups := reFuncNotDefinedError .FindStringSubmatch (err .Error ())
148
+ if len (groups ) != 5 {
149
+ return ""
150
+ }
151
+ tmplName , lineStr , message , funcName := groups [1 ], groups [2 ], groups [3 ], groups [4 ]
152
+ funcName , _ = strconv .Unquote (`"` + funcName + `"` )
153
+ return p .makeDetailedError (message , tmplName , lineStr , - 1 , funcName )
189
154
}
190
155
191
- const dashSeparator = "---------------------------------------------------------------------- \n "
156
+ var reUnexpectedOperandError = regexp . MustCompile ( `^template: (.*):([0-9]+): (unexpected "(.*)" in operand)` )
192
157
193
- // GetLineFromTemplate returns a line from a template with some context
194
- func GetLineFromTemplate (templateName string , targetLineNum int , target string , position int ) string {
195
- bs , err := AssetFS ().ReadFile (templateName + ".tmpl" )
196
- if err != nil {
197
- return fmt .Sprintf ("(unable to read template file: %v)" , err )
158
+ func (p * templateErrorPrettier ) handleUnexpectedOperandError (err error ) string {
159
+ groups := reUnexpectedOperandError .FindStringSubmatch (err .Error ())
160
+ if len (groups ) != 5 {
161
+ return ""
198
162
}
163
+ tmplName , lineStr , message , unexpected := groups [1 ], groups [2 ], groups [3 ], groups [4 ]
164
+ unexpected , _ = strconv .Unquote (`"` + unexpected + `"` )
165
+ return p .makeDetailedError (message , tmplName , lineStr , - 1 , unexpected )
166
+ }
199
167
200
- sb := & strings.Builder {}
201
-
202
- // Write the header
203
- sb .WriteString (dashSeparator )
168
+ var reExpectedEndError = regexp .MustCompile (`^template: (.*):([0-9]+): (expected end; found (.*))` )
204
169
205
- var lineBs []byte
170
+ func (p * templateErrorPrettier ) handleExpectedEndError (err error ) string {
171
+ groups := reExpectedEndError .FindStringSubmatch (err .Error ())
172
+ if len (groups ) != 5 {
173
+ return ""
174
+ }
175
+ tmplName , lineStr , message , unexpected := groups [1 ], groups [2 ], groups [3 ], groups [4 ]
176
+ return p .makeDetailedError (message , tmplName , lineStr , - 1 , unexpected )
177
+ }
206
178
207
- // Iterate through the lines from the asset file to find the target line
208
- for start , currentLineNum := 0 , 1 ; currentLineNum <= targetLineNum && start < len ( bs ); currentLineNum ++ {
209
- // Find the next new line
210
- end := bytes . IndexByte ( bs [ start :], '\n' )
179
+ var (
180
+ reTemplateExecutingError = regexp . MustCompile ( `^template: (.*):([1-9][0-9]*):([1-9][0-9]*): (executing .*)` )
181
+ reTemplateExecutingErrorMsg = regexp . MustCompile ( `^executing "(.*)" at <(.*)>: ` )
182
+ )
211
183
212
- // adjust the end to be a direct pointer in to []byte
213
- if end < 0 {
214
- end = len (bs )
215
- } else {
216
- end += start
184
+ func (p * templateErrorPrettier ) handleTemplateRenderingError (err error ) string {
185
+ if groups := reTemplateExecutingError .FindStringSubmatch (err .Error ()); len (groups ) > 0 {
186
+ tmplName , lineStr , posStr , msgPart := groups [1 ], groups [2 ], groups [3 ], groups [4 ]
187
+ target := ""
188
+ if groups = reTemplateExecutingErrorMsg .FindStringSubmatch (msgPart ); len (groups ) > 0 {
189
+ target = groups [2 ]
217
190
}
191
+ return p .makeDetailedError (msgPart , tmplName , lineStr , posStr , target )
192
+ } else if execErr , ok := err .(texttemplate.ExecError ); ok {
193
+ layerName := p .assets .GetFileLayerName (execErr .Name + ".tmpl" )
194
+ return fmt .Sprintf ("asset from: %s, %s" , layerName , err .Error ())
195
+ } else {
196
+ return err .Error ()
197
+ }
198
+ }
218
199
219
- // set lineBs to the current line []byte
220
- lineBs = bs [start :end ]
200
+ func HandleTemplateRenderingError (err error ) string {
201
+ p := & templateErrorPrettier {assets : AssetFS ()}
202
+ return p .handleTemplateRenderingError (err )
203
+ }
221
204
222
- // move start to after the current new line position
223
- start = end + 1
205
+ const dashSeparator = "----------------------------------------------------------------------"
224
206
225
- // Write 2 preceding lines + the target line
226
- if targetLineNum - currentLineNum < 3 {
227
- _ , _ = sb .Write (lineBs )
228
- _ = sb .WriteByte ('\n' )
229
- }
207
+ func (p * templateErrorPrettier ) makeDetailedError (errMsg , tmplName string , lineNum , posNum any , target string ) string {
208
+ code , layer , err := p .assets .ReadLayeredFile (tmplName + ".tmpl" )
209
+ if err != nil {
210
+ return fmt .Sprintf ("template error: %s, and unable to find template file %q" , errMsg , tmplName )
211
+ }
212
+ line , err := util .ToInt64 (lineNum )
213
+ if err != nil {
214
+ return fmt .Sprintf ("template error: %s, unable to parse template %q line number %q" , errMsg , tmplName , lineNum )
215
+ }
216
+ pos , err := util .ToInt64 (posNum )
217
+ if err != nil {
218
+ return fmt .Sprintf ("template error: %s, unable to parse template %q pos number %q" , errMsg , tmplName , posNum )
230
219
}
220
+ detail := extractErrorLine (code , int (line ), int (pos ), target )
231
221
232
- // FIXME: this algorithm could provide incorrect results and mislead the developers.
233
- // For example: Undefined function "file" in template .....
234
- // {{Func .file.Addition file.Deletion .file.Addition}}
235
- // ^^^^ ^(the real error is here)
236
- // The pointer is added to the first one, but the second one is the real incorrect one.
237
- //
238
- // If there is a provided target to look for in the line add a pointer to it
239
- // e.g. ^^^^^^^
240
- if target != "" {
241
- targetPos := bytes .Index (lineBs , []byte (target ))
242
- if targetPos >= 0 {
243
- position = targetPos
244
- }
222
+ var msg string
223
+ if pos >= 0 {
224
+ msg = fmt .Sprintf ("template error: %s:%s:%d:%d : %s" , layer , tmplName , line , pos , errMsg )
225
+ } else {
226
+ msg = fmt .Sprintf ("template error: %s:%s:%d : %s" , layer , tmplName , line , errMsg )
245
227
}
246
- if position >= 0 {
247
- // take the current line and replace preceding text with whitespace (except for tab)
248
- for i := range lineBs [:position ] {
249
- if lineBs [i ] != '\t' {
250
- lineBs [i ] = ' '
228
+ return msg + "\n " + dashSeparator + "\n " + detail + "\n " + dashSeparator
229
+ }
230
+
231
+ func extractErrorLine (code []byte , lineNum , posNum int , target string ) string {
232
+ b := bufio .NewReader (bytes .NewReader (code ))
233
+ var line []byte
234
+ var err error
235
+ for i := 0 ; i < lineNum ; i ++ {
236
+ if line , err = b .ReadBytes ('\n' ); err != nil {
237
+ if i == lineNum - 1 && errors .Is (err , io .EOF ) {
238
+ err = nil
251
239
}
240
+ break
252
241
}
242
+ }
243
+ if err != nil {
244
+ return fmt .Sprintf ("unable to find target line %d" , lineNum )
245
+ }
253
246
254
- // write the preceding "space"
255
- _ , _ = sb .Write (lineBs [:position ])
256
-
257
- // Now write the ^^ pointer
258
- targetLen := len (target )
259
- if targetLen == 0 {
260
- targetLen = 1
247
+ line = bytes .TrimRight (line , "\r \n " )
248
+ var indicatorLine []byte
249
+ targetBytes := []byte (target )
250
+ targetLen := len (targetBytes )
251
+ for i := 0 ; i < len (line ); {
252
+ if posNum == - 1 && target != "" && bytes .HasPrefix (line [i :], targetBytes ) {
253
+ for j := 0 ; j < targetLen && i < len (line ); j ++ {
254
+ indicatorLine = append (indicatorLine , '^' )
255
+ i ++
256
+ }
257
+ } else if i == posNum {
258
+ indicatorLine = append (indicatorLine , '^' )
259
+ i ++
260
+ } else {
261
+ if line [i ] == '\t' {
262
+ indicatorLine = append (indicatorLine , '\t' )
263
+ } else {
264
+ indicatorLine = append (indicatorLine , ' ' )
265
+ }
266
+ i ++
261
267
}
262
- _ , _ = sb .WriteString (strings .Repeat ("^" , targetLen ))
263
- _ = sb .WriteByte ('\n' )
264
268
}
265
-
266
- // Finally write the footer
267
- sb .WriteString (dashSeparator )
268
-
269
- return sb .String ()
269
+ // if the indicatorLine only contains spaces, trim it together
270
+ return strings .TrimRight (string (line )+ "\n " + string (indicatorLine ), " \t \r \n " )
270
271
}
0 commit comments