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

highlight.go 4.2 kB

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