@@ -12,7 +12,6 @@ import (
1212 "html/template"
1313 "io"
1414 "path"
15- "path/filepath"
1615 "strings"
1716 "sync"
1817
@@ -25,35 +24,32 @@ import (
2524 "github.com/alecthomas/chroma/v2/formatters/html"
2625 "github.com/alecthomas/chroma/v2/lexers"
2726 "github.com/alecthomas/chroma/v2/styles"
28- lru "github.com/hashicorp/golang-lru /v2"
27+ "github.com/go-enry/go-enry /v2"
2928)
3029
3130// don't index files larger than this many bytes for performance purposes
3231const sizeLimit = 1024 * 1024
3332
34- var (
35- // For custom user mapping
36- highlightMapping = map [string ]string {}
37-
38- once sync.Once
39-
40- cache * lru.TwoQueueCache [string , any ]
33+ type globalVarsType struct {
34+ highlightMapping map [string ]string
35+ githubStyles * chroma.Style
36+ }
4137
42- githubStyles = styles .Get ("github" )
38+ var (
39+ globalVarsMu sync.Mutex
40+ globalVarsPtr * globalVarsType
4341)
4442
45- // NewContext loads custom highlight map from local config
46- func NewContext () {
47- once .Do (func () {
48- highlightMapping = setting .GetHighlightMapping ()
49-
50- // The size 512 is simply a conservative rule of thumb
51- c , err := lru.New2Q [string , any ](512 )
52- if err != nil {
53- panic (fmt .Sprintf ("failed to initialize LRU cache for highlighter: %s" , err ))
54- }
55- cache = c
56- })
43+ func globalVars () * globalVarsType {
44+ // in the future, the globalVars might need to be re-initialized when settings change, so don't use sync.Once here
45+ globalVarsMu .Lock ()
46+ defer globalVarsMu .Unlock ()
47+ if globalVarsPtr == nil {
48+ globalVarsPtr = & globalVarsType {}
49+ globalVarsPtr .githubStyles = styles .Get ("github" )
50+ globalVarsPtr .highlightMapping = setting .GetHighlightMapping ()
51+ }
52+ return globalVarsPtr
5753}
5854
5955// UnsafeSplitHighlightedLines splits highlighted code into lines preserving HTML tags
@@ -88,59 +84,85 @@ func UnsafeSplitHighlightedLines(code template.HTML) (ret [][]byte) {
8884 }
8985}
9086
91- // Code returns an HTML version of code string with chroma syntax highlighting classes and the matched lexer name
92- func Code (fileName , language , code string ) (output template.HTML , lexerName string ) {
93- NewContext ()
94-
95- // diff view newline will be passed as empty, change to literal '\n' so it can be copied
96- // preserve literal newline in blame view
97- if code == "" || code == "\n " {
98- return "\n " , ""
87+ func getChromaLexerByLanguage (fileName , lang string ) chroma.Lexer {
88+ lang , _ , _ = strings .Cut (lang , "?" ) // maybe, the value from gitattributes might contain `?` parameters?
89+ ext := path .Ext (fileName )
90+ // the "lang" might come from enry, it has different naming for some languages
91+ switch lang {
92+ case "F#" :
93+ lang = "FSharp"
94+ case "Pascal" :
95+ lang = "ObjectPascal"
96+ case "C" :
97+ if ext == ".C" || ext == ".H" {
98+ lang = "C++"
99+ }
99100 }
101+ // lexers.Get is slow if the language name can't be matched directly: it does extra "Match" call to iterate all lexers
102+ return lexers .Get (lang )
103+ }
100104
101- if len (code ) > sizeLimit {
102- return template .HTML (template .HTMLEscapeString (code )), ""
105+ // GetChromaLexerWithFallback returns a chroma lexer by given file name, language and code content. All parameters can be optional.
106+ // When code content is provided, it will be slow if no lexer is found by file name or language.
107+ // If no lexer is found, it will return the fallback lexer.
108+ func GetChromaLexerWithFallback (fileName , lang string , code []byte ) (lexer chroma.Lexer ) {
109+ if lang != "" {
110+ lexer = getChromaLexerByLanguage (fileName , lang )
103111 }
104112
105- var lexer chroma.Lexer
106-
107- if len (language ) > 0 {
108- lexer = lexers .Get (language )
113+ if lexer == nil {
114+ fileExt := path .Ext (fileName )
115+ if val , ok := globalVars ().highlightMapping [fileExt ]; ok {
116+ lexer = getChromaLexerByLanguage (fileName , val ) // use mapped value to find lexer
117+ }
118+ }
109119
120+ if lexer == nil {
121+ // when using "code" to detect, analyze.GetCodeLanguage is slower, it iterates many rules to detect language from content
122+ // this is the old logic: use enry to detect language, and use chroma to render, but their naming is different for some languages
123+ enryLanguage := analyze .GetCodeLanguage (fileName , code )
124+ lexer = getChromaLexerByLanguage (fileName , enryLanguage )
110125 if lexer == nil {
111- // Attempt stripping off the '?'
112- if before , _ , ok := strings .Cut (language , "?" ); ok {
113- lexer = lexers .Get (before )
126+ if enryLanguage != enry .OtherLanguage {
127+ log .Warn ("No chroma lexer found for enry detected language: %s (file: %s), need to fix the language mapping between enry and chroma." , enryLanguage , fileName )
114128 }
129+ lexer = lexers .Match (fileName ) // lexers.Match will search by its basename and extname
115130 }
116131 }
117132
118- if lexer == nil {
119- if val , ok := highlightMapping [path .Ext (fileName )]; ok {
120- // use mapped value to find lexer
121- lexer = lexers .Get (val )
122- }
133+ return util .IfZero (lexer , lexers .Fallback )
134+ }
135+
136+ func renderCode (fileName , language , code string , slowGuess bool ) (output template.HTML , lexerName string ) {
137+ // diff view newline will be passed as empty, change to literal '\n' so it can be copied
138+ // preserve literal newline in blame view
139+ if code == "" || code == "\n " {
140+ return "\n " , ""
123141 }
124142
125- if lexer == nil {
126- if l , ok := cache .Get (fileName ); ok {
127- lexer = l .(chroma.Lexer )
128- }
143+ if len (code ) > sizeLimit {
144+ return template .HTML (template .HTMLEscapeString (code )), ""
129145 }
130146
131- if lexer == nil {
132- lexer = lexers .Match (fileName )
133- if lexer == nil {
134- lexer = lexers .Fallback
135- }
136- cache .Add (fileName , lexer )
147+ var codeForGuessLexer []byte
148+ if slowGuess {
149+ // it is slower to guess lexer by code content, so only do it when necessary
150+ codeForGuessLexer = util .UnsafeStringToBytes (code )
137151 }
152+ lexer := GetChromaLexerWithFallback (fileName , language , codeForGuessLexer )
153+ return RenderCodeByLexer (lexer , code ), formatLexerName (lexer .Config ().Name )
154+ }
138155
139- return CodeFromLexer (lexer , code ), formatLexerName (lexer .Config ().Name )
156+ func RenderCodeFast (fileName , language , code string ) (output template.HTML , lexerName string ) {
157+ return renderCode (fileName , language , code , false )
140158}
141159
142- // CodeFromLexer returns a HTML version of code string with chroma syntax highlighting classes
143- func CodeFromLexer (lexer chroma.Lexer , code string ) template.HTML {
160+ func RenderCodeSlowGuess (fileName , language , code string ) (output template.HTML , lexerName string ) {
161+ return renderCode (fileName , language , code , true )
162+ }
163+
164+ // RenderCodeByLexer returns a HTML version of code string with chroma syntax highlighting classes
165+ func RenderCodeByLexer (lexer chroma.Lexer , code string ) template.HTML {
144166 formatter := html .New (html .WithClasses (true ),
145167 html .WithLineNumbers (false ),
146168 html .PreventSurroundingPre (true ),
@@ -155,7 +177,7 @@ func CodeFromLexer(lexer chroma.Lexer, code string) template.HTML {
155177 return template .HTML (template .HTMLEscapeString (code ))
156178 }
157179 // style not used for live site but need to pass something
158- err = formatter .Format (htmlw , githubStyles , iterator )
180+ err = formatter .Format (htmlw , globalVars (). githubStyles , iterator )
159181 if err != nil {
160182 log .Error ("Can't format code: %v" , err )
161183 return template .HTML (template .HTMLEscapeString (code ))
@@ -167,44 +189,18 @@ func CodeFromLexer(lexer chroma.Lexer, code string) template.HTML {
167189 return template .HTML (strings .TrimSuffix (htmlbuf .String (), "\n " ))
168190}
169191
170- // File returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
171- func File (fileName , language string , code []byte ) ([]template.HTML , string , error ) {
172- NewContext ()
173-
192+ // RenderFullFile returns a slice of chroma syntax highlighted HTML lines of code and the matched lexer name
193+ func RenderFullFile (fileName , language string , code []byte ) ([]template.HTML , string , error ) {
174194 if len (code ) > sizeLimit {
175- return PlainText (code ), "" , nil
195+ return RenderPlainText (code ), "" , nil
176196 }
177197
178198 formatter := html .New (html .WithClasses (true ),
179199 html .WithLineNumbers (false ),
180200 html .PreventSurroundingPre (true ),
181201 )
182202
183- var lexer chroma.Lexer
184-
185- // provided language overrides everything
186- if language != "" {
187- lexer = lexers .Get (language )
188- }
189-
190- if lexer == nil {
191- if val , ok := highlightMapping [filepath .Ext (fileName )]; ok {
192- lexer = lexers .Get (val )
193- }
194- }
195-
196- if lexer == nil {
197- guessLanguage := analyze .GetCodeLanguage (fileName , code )
198-
199- lexer = lexers .Get (guessLanguage )
200- if lexer == nil {
201- lexer = lexers .Match (fileName )
202- if lexer == nil {
203- lexer = lexers .Fallback
204- }
205- }
206- }
207-
203+ lexer := GetChromaLexerWithFallback (fileName , language , code )
208204 lexerName := formatLexerName (lexer .Config ().Name )
209205
210206 iterator , err := lexer .Tokenise (nil , string (code ))
@@ -218,7 +214,7 @@ func File(fileName, language string, code []byte) ([]template.HTML, string, erro
218214 lines := make ([]template.HTML , 0 , len (tokensLines ))
219215 for _ , tokens := range tokensLines {
220216 iterator = chroma .Literator (tokens ... )
221- err = formatter .Format (htmlBuf , githubStyles , iterator )
217+ err = formatter .Format (htmlBuf , globalVars (). githubStyles , iterator )
222218 if err != nil {
223219 return nil , "" , fmt .Errorf ("can't format code: %w" , err )
224220 }
@@ -229,8 +225,8 @@ func File(fileName, language string, code []byte) ([]template.HTML, string, erro
229225 return lines , lexerName , nil
230226}
231227
232- // PlainText returns non-highlighted HTML for code
233- func PlainText (code []byte ) []template.HTML {
228+ // RenderPlainText returns non-highlighted HTML for code
229+ func RenderPlainText (code []byte ) []template.HTML {
234230 r := bufio .NewReader (bytes .NewReader (code ))
235231 m := make ([]template.HTML , 0 , bytes .Count (code , []byte {'\n' })+ 1 )
236232 for {
0 commit comments