github.com/gitbundle/modules@v0.0.0-20231025071548-85b91c5c3b01/highlight/highlight.go (about) 1 // Copyright 2023 The GitBundle Inc. All rights reserved. 2 // Copyright 2017 The Gitea Authors. All rights reserved. 3 // Use of this source code is governed by a MIT-style 4 // license that can be found in the LICENSE file. 5 6 // Copyright 2015 The Gogs Authors. All rights reserved. 7 8 package highlight 9 10 import ( 11 "bufio" 12 "bytes" 13 "fmt" 14 gohtml "html" 15 "path/filepath" 16 "strings" 17 "sync" 18 19 "github.com/gitbundle/modules/analyze" 20 "github.com/gitbundle/modules/log" 21 "github.com/gitbundle/modules/setting" 22 23 "github.com/alecthomas/chroma" 24 "github.com/alecthomas/chroma/formatters/html" 25 "github.com/alecthomas/chroma/lexers" 26 "github.com/alecthomas/chroma/styles" 27 lru "github.com/hashicorp/golang-lru" 28 ) 29 30 // don't index files larger than this many bytes for performance purposes 31 const sizeLimit = 1024 * 1024 32 33 var ( 34 // For custom user mapping 35 highlightMapping = map[string]string{} 36 37 once sync.Once 38 39 cache *lru.TwoQueueCache 40 ) 41 42 // NewContext loads custom highlight map from local config 43 func NewContext() { 44 once.Do(func() { 45 keys := setting.Cfg.Section("highlight.mapping").Keys() 46 for i := range keys { 47 highlightMapping[keys[i].Name()] = keys[i].Value() 48 } 49 50 // The size 512 is simply a conservative rule of thumb 51 c, err := lru.New2Q(512) 52 if err != nil { 53 panic(fmt.Sprintf("failed to initialize LRU cache for highlighter: %s", err)) 54 } 55 cache = c 56 }) 57 } 58 59 // Code returns a HTML version of code string with chroma syntax highlighting classes 60 func Code(fileName, language, code string) string { 61 NewContext() 62 63 // diff view newline will be passed as empty, change to literal '\n' so it can be copied 64 // preserve literal newline in blame view 65 if code == "" || code == "\n" { 66 return "\n" 67 } 68 69 if len(code) > sizeLimit { 70 return code 71 } 72 73 var lexer chroma.Lexer 74 75 if len(language) > 0 { 76 lexer = lexers.Get(language) 77 78 if lexer == nil { 79 // Attempt stripping off the '?' 80 if idx := strings.IndexByte(language, '?'); idx > 0 { 81 lexer = lexers.Get(language[:idx]) 82 } 83 } 84 } 85 86 if lexer == nil { 87 if val, ok := highlightMapping[filepath.Ext(fileName)]; ok { 88 // use mapped value to find lexer 89 lexer = lexers.Get(val) 90 } 91 } 92 93 if lexer == nil { 94 if l, ok := cache.Get(fileName); ok { 95 lexer = l.(chroma.Lexer) 96 } 97 } 98 99 if lexer == nil { 100 lexer = lexers.Match(fileName) 101 if lexer == nil { 102 lexer = lexers.Fallback 103 } 104 cache.Add(fileName, lexer) 105 } 106 return CodeFromLexer(lexer, code) 107 } 108 109 type nopPreWrapper struct{} 110 111 func (nopPreWrapper) Start(code bool, styleAttr string) string { return "" } 112 func (nopPreWrapper) End(code bool) string { return "" } 113 114 // CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes 115 func CodeFromLexer(lexer chroma.Lexer, code string) string { 116 formatter := html.New(html.WithClasses(true), 117 html.WithLineNumbers(false), 118 html.PreventSurroundingPre(true), 119 ) 120 121 htmlbuf := bytes.Buffer{} 122 htmlw := bufio.NewWriter(&htmlbuf) 123 124 iterator, err := lexer.Tokenise(nil, string(code)) 125 if err != nil { 126 log.Error("Can't tokenize code: %v", err) 127 return code 128 } 129 // style not used for live site but need to pass something 130 err = formatter.Format(htmlw, styles.GitHub, iterator) 131 if err != nil { 132 log.Error("Can't format code: %v", err) 133 return code 134 } 135 136 _ = htmlw.Flush() 137 // Chroma will add newlines for certain lexers in order to highlight them properly 138 // Once highlighted, strip them here, so they don't cause copy/paste trouble in HTML output 139 return strings.TrimSuffix(htmlbuf.String(), "\n") 140 } 141 142 // File returns a slice of chroma syntax highlighted lines of code 143 func File(numLines int, fileName, language string, code []byte) []string { 144 NewContext() 145 146 if len(code) > sizeLimit { 147 return plainText(string(code), numLines) 148 } 149 formatter := html.New(html.WithClasses(true), 150 html.WithLineNumbers(false), 151 html.WithPreWrapper(nopPreWrapper{}), 152 ) 153 154 if formatter == nil { 155 log.Error("Couldn't create chroma formatter") 156 return plainText(string(code), numLines) 157 } 158 159 htmlbuf := bytes.Buffer{} 160 htmlw := bufio.NewWriter(&htmlbuf) 161 162 var lexer chroma.Lexer 163 164 // provided language overrides everything 165 if len(language) > 0 { 166 lexer = lexers.Get(language) 167 } 168 169 if lexer == nil { 170 if val, ok := highlightMapping[filepath.Ext(fileName)]; ok { 171 lexer = lexers.Get(val) 172 } 173 } 174 175 if lexer == nil { 176 language := analyze.GetCodeLanguage(fileName, code) 177 178 lexer = lexers.Get(language) 179 if lexer == nil { 180 lexer = lexers.Match(fileName) 181 if lexer == nil { 182 lexer = lexers.Fallback 183 } 184 } 185 } 186 187 iterator, err := lexer.Tokenise(nil, string(code)) 188 if err != nil { 189 log.Error("Can't tokenize code: %v", err) 190 return plainText(string(code), numLines) 191 } 192 193 err = formatter.Format(htmlw, styles.GitHub, iterator) 194 if err != nil { 195 log.Error("Can't format code: %v", err) 196 return plainText(string(code), numLines) 197 } 198 199 _ = htmlw.Flush() 200 finalNewLine := false 201 if len(code) > 0 { 202 finalNewLine = code[len(code)-1] == '\n' 203 } 204 205 m := strings.SplitN(htmlbuf.String(), `</span></span><span class="line"><span class="cl">`, numLines) 206 if len(m) > 0 { 207 m[0] = m[0][len(`<span class="line"><span class="cl">`):] 208 last := m[len(m)-1] 209 m[len(m)-1] = last[:len(last)-len(`</span></span>`)] 210 } 211 212 if finalNewLine { 213 m = append(m, "<span class=\"w\">\n</span>") 214 } 215 216 return m 217 } 218 219 // return unhiglighted map 220 func plainText(code string, numLines int) []string { 221 m := strings.SplitN(code, "\n", numLines) 222 223 for i, content := range m { 224 // need to keep lines that are only \n so copy/paste works properly in browser 225 if content == "" { 226 content = "\n" 227 } 228 m[i] = gohtml.EscapeString(content) 229 } 230 return m 231 }