code.gitea.io/gitea@v1.19.3/modules/templates/htmlrenderer.go (about) 1 // Copyright 2022 The Gitea Authors. All rights reserved. 2 // SPDX-License-Identifier: MIT 3 4 package templates 5 6 import ( 7 "bytes" 8 "context" 9 "fmt" 10 "regexp" 11 "strconv" 12 "strings" 13 14 "code.gitea.io/gitea/modules/log" 15 "code.gitea.io/gitea/modules/setting" 16 "code.gitea.io/gitea/modules/watcher" 17 18 "github.com/unrolled/render" 19 ) 20 21 var ( 22 rendererKey interface{} = "templatesHtmlRenderer" 23 24 templateError = regexp.MustCompile(`^template: (.*):([0-9]+): (.*)`) 25 notDefinedError = regexp.MustCompile(`^template: (.*):([0-9]+): function "(.*)" not defined`) 26 unexpectedError = regexp.MustCompile(`^template: (.*):([0-9]+): unexpected "(.*)" in operand`) 27 expectedEndError = regexp.MustCompile(`^template: (.*):([0-9]+): expected end; found (.*)`) 28 ) 29 30 // HTMLRenderer returns the current html renderer for the context or creates and stores one within the context for future use 31 func HTMLRenderer(ctx context.Context) (context.Context, *render.Render) { 32 rendererInterface := ctx.Value(rendererKey) 33 if rendererInterface != nil { 34 renderer, ok := rendererInterface.(*render.Render) 35 if ok { 36 return ctx, renderer 37 } 38 } 39 40 rendererType := "static" 41 if !setting.IsProd { 42 rendererType = "auto-reloading" 43 } 44 log.Log(1, log.DEBUG, "Creating "+rendererType+" HTML Renderer") 45 46 compilingTemplates := true 47 defer func() { 48 if !compilingTemplates { 49 return 50 } 51 52 panicked := recover() 53 if panicked == nil { 54 return 55 } 56 57 // OK try to handle the panic... 58 err, ok := panicked.(error) 59 if ok { 60 handlePanicError(err) 61 } 62 log.Fatal("PANIC: Unable to compile templates!\n%v\n\nStacktrace:\n%s", panicked, log.Stack(2)) 63 }() 64 65 renderer := render.New(render.Options{ 66 Extensions: []string{".tmpl"}, 67 Directory: "templates", 68 Funcs: NewFuncMap(), 69 Asset: GetAsset, 70 AssetNames: GetTemplateAssetNames, 71 UseMutexLock: !setting.IsProd, 72 IsDevelopment: false, 73 DisableHTTPErrorRendering: true, 74 }) 75 compilingTemplates = false 76 if !setting.IsProd { 77 watcher.CreateWatcher(ctx, "HTML Templates", &watcher.CreateWatcherOpts{ 78 PathsCallback: walkTemplateFiles, 79 BetweenCallback: func() { 80 defer func() { 81 if err := recover(); err != nil { 82 log.Error("PANIC: %v\n%s", err, log.Stack(2)) 83 } 84 }() 85 renderer.CompileTemplates() 86 }, 87 }) 88 } 89 return context.WithValue(ctx, rendererKey, renderer), renderer 90 } 91 92 func handlePanicError(err error) { 93 wrapFatal(handleNotDefinedPanicError(err)) 94 wrapFatal(handleUnexpected(err)) 95 wrapFatal(handleExpectedEnd(err)) 96 wrapFatal(handleGenericTemplateError(err)) 97 } 98 99 func wrapFatal(format string, args []interface{}) { 100 if format == "" { 101 return 102 } 103 log.FatalWithSkip(1, format, args...) 104 } 105 106 func handleGenericTemplateError(err error) (string, []interface{}) { 107 groups := templateError.FindStringSubmatch(err.Error()) 108 if len(groups) != 4 { 109 return "", nil 110 } 111 112 templateName, lineNumberStr, message := groups[1], groups[2], groups[3] 113 114 filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl") 115 if assetErr != nil { 116 return "", nil 117 } 118 119 lineNumber, _ := strconv.Atoi(lineNumberStr) 120 121 line := GetLineFromTemplate(templateName, lineNumber, "", -1) 122 123 return "PANIC: Unable to compile templates!\n%s in template file %s at line %d:\n\n%s\nStacktrace:\n\n%s", []interface{}{message, filename, lineNumber, log.NewColoredValue(line, log.Reset), log.Stack(2)} 124 } 125 126 func handleNotDefinedPanicError(err error) (string, []interface{}) { 127 groups := notDefinedError.FindStringSubmatch(err.Error()) 128 if len(groups) != 4 { 129 return "", nil 130 } 131 132 templateName, lineNumberStr, functionName := groups[1], groups[2], groups[3] 133 134 functionName, _ = strconv.Unquote(`"` + functionName + `"`) 135 136 filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl") 137 if assetErr != nil { 138 return "", nil 139 } 140 141 lineNumber, _ := strconv.Atoi(lineNumberStr) 142 143 line := GetLineFromTemplate(templateName, lineNumber, functionName, -1) 144 145 return "PANIC: Unable to compile templates!\nUndefined function %q in template file %s at line %d:\n\n%s", []interface{}{functionName, filename, lineNumber, log.NewColoredValue(line, log.Reset)} 146 } 147 148 func handleUnexpected(err error) (string, []interface{}) { 149 groups := unexpectedError.FindStringSubmatch(err.Error()) 150 if len(groups) != 4 { 151 return "", nil 152 } 153 154 templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] 155 unexpected, _ = strconv.Unquote(`"` + unexpected + `"`) 156 157 filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl") 158 if assetErr != nil { 159 return "", nil 160 } 161 162 lineNumber, _ := strconv.Atoi(lineNumberStr) 163 164 line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1) 165 166 return "PANIC: Unable to compile templates!\nUnexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)} 167 } 168 169 func handleExpectedEnd(err error) (string, []interface{}) { 170 groups := expectedEndError.FindStringSubmatch(err.Error()) 171 if len(groups) != 4 { 172 return "", nil 173 } 174 175 templateName, lineNumberStr, unexpected := groups[1], groups[2], groups[3] 176 177 filename, assetErr := GetAssetFilename("templates/" + templateName + ".tmpl") 178 if assetErr != nil { 179 return "", nil 180 } 181 182 lineNumber, _ := strconv.Atoi(lineNumberStr) 183 184 line := GetLineFromTemplate(templateName, lineNumber, unexpected, -1) 185 186 return "PANIC: Unable to compile templates!\nMissing end with unexpected %q in template file %s at line %d:\n\n%s", []interface{}{unexpected, filename, lineNumber, log.NewColoredValue(line, log.Reset)} 187 } 188 189 const dashSeparator = "----------------------------------------------------------------------\n" 190 191 // GetLineFromTemplate returns a line from a template with some context 192 func GetLineFromTemplate(templateName string, targetLineNum int, target string, position int) string { 193 bs, err := GetAsset("templates/" + templateName + ".tmpl") 194 if err != nil { 195 return fmt.Sprintf("(unable to read template file: %v)", err) 196 } 197 198 sb := &strings.Builder{} 199 200 // Write the header 201 sb.WriteString(dashSeparator) 202 203 var lineBs []byte 204 205 // Iterate through the lines from the asset file to find the target line 206 for start, currentLineNum := 0, 1; currentLineNum <= targetLineNum && start < len(bs); currentLineNum++ { 207 // Find the next new line 208 end := bytes.IndexByte(bs[start:], '\n') 209 210 // adjust the end to be a direct pointer in to []byte 211 if end < 0 { 212 end = len(bs) 213 } else { 214 end += start 215 } 216 217 // set lineBs to the current line []byte 218 lineBs = bs[start:end] 219 220 // move start to after the current new line position 221 start = end + 1 222 223 // Write 2 preceding lines + the target line 224 if targetLineNum-currentLineNum < 3 { 225 _, _ = sb.Write(lineBs) 226 _ = sb.WriteByte('\n') 227 } 228 } 229 230 // If there is a provided target to look for in the line add a pointer to it 231 // e.g. ^^^^^^^ 232 if target != "" { 233 targetPos := bytes.Index(lineBs, []byte(target)) 234 if targetPos >= 0 { 235 position = targetPos 236 } 237 } 238 if position >= 0 { 239 // take the current line and replace preceding text with whitespace (except for tab) 240 for i := range lineBs[:position] { 241 if lineBs[i] != '\t' { 242 lineBs[i] = ' ' 243 } 244 } 245 246 // write the preceding "space" 247 _, _ = sb.Write(lineBs[:position]) 248 249 // Now write the ^^ pointer 250 targetLen := len(target) 251 if targetLen == 0 { 252 targetLen = 1 253 } 254 _, _ = sb.WriteString(strings.Repeat("^", targetLen)) 255 _ = sb.WriteByte('\n') 256 } 257 258 // Finally write the footer 259 sb.WriteString(dashSeparator) 260 261 return sb.String() 262 }