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  }