You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

highlight.go 3.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2020 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. package highlight
  6. import (
  7. "bufio"
  8. "bytes"
  9. "path/filepath"
  10. "strings"
  11. "sync"
  12. "code.gitea.io/gitea/modules/log"
  13. "code.gitea.io/gitea/modules/setting"
  14. "github.com/alecthomas/chroma"
  15. "github.com/alecthomas/chroma/formatters/html"
  16. "github.com/alecthomas/chroma/lexers"
  17. "github.com/alecthomas/chroma/styles"
  18. )
  19. // don't index files larger than this many bytes for performance purposes
  20. const sizeLimit = 1000000
  21. var (
  22. // For custom user mapping
  23. highlightMapping = map[string]string{}
  24. once sync.Once
  25. )
  26. // NewContext loads custom highlight map from local config
  27. func NewContext() {
  28. once.Do(func() {
  29. keys := setting.Cfg.Section("highlight.mapping").Keys()
  30. for i := range keys {
  31. highlightMapping[keys[i].Name()] = keys[i].Value()
  32. }
  33. })
  34. }
  35. // Code returns a HTML version of code string with chroma syntax highlighting classes
  36. func Code(fileName, code string) string {
  37. NewContext()
  38. if code == "" {
  39. return "\n"
  40. }
  41. if len(code) > sizeLimit {
  42. return code
  43. }
  44. formatter := html.New(html.WithClasses(true),
  45. html.WithLineNumbers(false),
  46. html.PreventSurroundingPre(true),
  47. )
  48. if formatter == nil {
  49. log.Error("Couldn't create chroma formatter")
  50. return code
  51. }
  52. htmlbuf := bytes.Buffer{}
  53. htmlw := bufio.NewWriter(&htmlbuf)
  54. if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
  55. //change file name to one with mapped extension so we look that up instead
  56. fileName = "mapped." + val
  57. }
  58. lexer := lexers.Match(fileName)
  59. if lexer == nil {
  60. lexer = lexers.Fallback
  61. }
  62. iterator, err := lexer.Tokenise(&chroma.TokeniseOptions{State: "root", Nested: true}, string(code))
  63. if err != nil {
  64. log.Error("Can't tokenize code: %v", err)
  65. return code
  66. }
  67. // style not used for live site but need to pass something
  68. err = formatter.Format(htmlw, styles.GitHub, iterator)
  69. if err != nil {
  70. log.Error("Can't format code: %v", err)
  71. return code
  72. }
  73. htmlw.Flush()
  74. return htmlbuf.String()
  75. }
  76. // File returns map with line lumbers and HTML version of code with chroma syntax highlighting classes
  77. func File(numLines int, fileName string, code []byte) map[int]string {
  78. NewContext()
  79. if len(code) > sizeLimit {
  80. return plainText(string(code), numLines)
  81. }
  82. formatter := html.New(html.WithClasses(true),
  83. html.WithLineNumbers(false),
  84. html.PreventSurroundingPre(true),
  85. )
  86. if formatter == nil {
  87. log.Error("Couldn't create chroma formatter")
  88. return plainText(string(code), numLines)
  89. }
  90. htmlbuf := bytes.Buffer{}
  91. htmlw := bufio.NewWriter(&htmlbuf)
  92. if val, ok := highlightMapping[filepath.Ext(fileName)]; ok {
  93. fileName = "test." + val
  94. }
  95. lexer := lexers.Match(fileName)
  96. if lexer == nil {
  97. lexer = lexers.Analyse(string(code))
  98. if lexer == nil {
  99. lexer = lexers.Fallback
  100. }
  101. }
  102. iterator, err := lexer.Tokenise(nil, string(code))
  103. if err != nil {
  104. log.Error("Can't tokenize code: %v", err)
  105. return plainText(string(code), numLines)
  106. }
  107. err = formatter.Format(htmlw, styles.GitHub, iterator)
  108. if err != nil {
  109. log.Error("Can't format code: %v", err)
  110. return plainText(string(code), numLines)
  111. }
  112. htmlw.Flush()
  113. m := make(map[int]string, numLines)
  114. for k, v := range strings.SplitN(htmlbuf.String(), "\n", numLines) {
  115. line := k + 1
  116. content := string(v)
  117. //need to keep lines that are only \n so copy/paste works properly in browser
  118. if content == "" {
  119. content = "\n"
  120. }
  121. m[line] = content
  122. }
  123. return m
  124. }
  125. // return unhiglighted map
  126. func plainText(code string, numLines int) map[int]string {
  127. m := make(map[int]string, numLines)
  128. for k, v := range strings.SplitN(string(code), "\n", numLines) {
  129. line := k + 1
  130. content := string(v)
  131. //need to keep lines that are only \n so copy/paste works properly in browser
  132. if content == "" {
  133. content = "\n"
  134. }
  135. m[line] = content
  136. }
  137. return m
  138. }