diff options
author | mrsdizzie <info@mrsdizzie.com> | 2020-06-30 17:34:03 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-01 00:34:03 +0300 |
commit | af7ffaa2798148e2a1b249da2330200bc032d7b1 (patch) | |
tree | 4f1f41767fa620dff4142ac7ebcd74b0abd61033 /modules | |
parent | ce5f2b9845659efaca0b81998dca6cf03882b134 (diff) | |
download | gitea-af7ffaa2798148e2a1b249da2330200bc032d7b1.tar.gz gitea-af7ffaa2798148e2a1b249da2330200bc032d7b1.zip |
Server-side syntax highlighting for all code (#12047)
* Server-side syntax hilighting for all code
This PR does a few things:
* Remove all traces of highlight.js
* Use chroma library to provide fast syntax hilighting directly on the server
* Provide syntax hilighting for diffs
* Re-style both unified and split diffs views
* Add custom syntax hilighting styling for both regular and arc-green
Fixes #7729
Fixes #10157
Fixes #11825
Fixes #7728
Fixes #3872
Fixes #3682
And perhaps gets closer to #9553
* fix line marker
* fix repo search
* Fix single line select
* properly load settings
* npm uninstall highlight.js
* review suggestion
* code review
* forgot to call function
* fix test
* Apply suggestions from code review
suggestions from @silverwind thanks
Co-authored-by: silverwind <me@silverwind.io>
* code review
* copy/paste error
* Use const for highlight size limit
* Update web_src/less/_repository.less
Co-authored-by: Lauris BH <lauris@nix.lv>
* update size limit to 1MB and other styling tweaks
* fix highlighting for certain diff sections
* fix test
* add worker back as suggested
Co-authored-by: silverwind <me@silverwind.io>
Co-authored-by: Lauris BH <lauris@nix.lv>
Diffstat (limited to 'modules')
-rw-r--r-- | modules/highlight/highlight.go | 235 | ||||
-rw-r--r-- | modules/indexer/code/search.go | 22 | ||||
-rw-r--r-- | modules/markup/markdown/markdown.go | 26 | ||||
-rw-r--r-- | modules/markup/sanitizer.go | 8 | ||||
-rw-r--r-- | modules/repofiles/diff_test.go | 3 |
5 files changed, 154 insertions, 140 deletions
diff --git a/modules/highlight/highlight.go b/modules/highlight/highlight.go index ffd88656ae..90590d220b 100644 --- a/modules/highlight/highlight.go +++ b/modules/highlight/highlight.go @@ -1,151 +1,148 @@ // Copyright 2015 The Gogs Authors. All rights reserved. +// Copyright 2020 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package highlight import ( - "path" + "bufio" + "bytes" + "path/filepath" "strings" + "sync" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "github.com/alecthomas/chroma/formatters/html" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" ) +// don't index files larger than this many bytes for performance purposes +const sizeLimit = 1000000 + var ( - // File name should ignore highlight. - ignoreFileNames = map[string]bool{ - "license": true, - "copying": true, - } + // For custom user mapping + highlightMapping = map[string]string{} + + once sync.Once +) + +// NewContext loads custom highlight map from local config +func NewContext() { + once.Do(func() { + keys := setting.Cfg.Section("highlight.mapping").Keys() + for i := range keys { + highlightMapping[keys[i].Name()] = keys[i].Value() + } + }) +} + +// Code returns a HTML version of code string with chroma syntax highlighting classes +func Code(fileName, code string) string { + NewContext() - // File names that are representing highlight classes. - highlightFileNames = map[string]string{ - "dockerfile": "dockerfile", - "makefile": "makefile", - "gnumakefile": "makefile", - "cmakelists.txt": "cmake", + if len(code) > sizeLimit { + return code + } + formatter := html.New(html.WithClasses(true), + html.WithLineNumbers(false), + html.PreventSurroundingPre(true), + ) + if formatter == nil { + log.Error("Couldn't create chroma formatter") + return code } - // Extensions that are same as highlight classes. - // See hljs.listLanguages() for list of language names. - highlightExts = map[string]struct{}{ - ".applescript": {}, - ".arm": {}, - ".as": {}, - ".bash": {}, - ".bat": {}, - ".c": {}, - ".cmake": {}, - ".cpp": {}, - ".cs": {}, - ".css": {}, - ".dart": {}, - ".diff": {}, - ".django": {}, - ".go": {}, - ".gradle": {}, - ".groovy": {}, - ".haml": {}, - ".handlebars": {}, - ".html": {}, - ".ini": {}, - ".java": {}, - ".json": {}, - ".less": {}, - ".lua": {}, - ".php": {}, - ".scala": {}, - ".scss": {}, - ".sql": {}, - ".swift": {}, - ".ts": {}, - ".xml": {}, - ".yaml": {}, + htmlbuf := bytes.Buffer{} + htmlw := bufio.NewWriter(&htmlbuf) + + if val, ok := highlightMapping[filepath.Ext(fileName)]; ok { + //change file name to one with mapped extension so we look that up instead + fileName = "mapped." + val } - // Extensions that are not same as highlight classes. - highlightMapping = map[string]string{ - ".ahk": "autohotkey", - ".crmsh": "crmsh", - ".dash": "shell", - ".erl": "erlang", - ".escript": "erlang", - ".ex": "elixir", - ".exs": "elixir", - ".f": "fortran", - ".f77": "fortran", - ".f90": "fortran", - ".f95": "fortran", - ".feature": "gherkin", - ".fish": "shell", - ".for": "fortran", - ".hbs": "handlebars", - ".hs": "haskell", - ".hx": "haxe", - ".js": "javascript", - ".jsx": "javascript", - ".ksh": "shell", - ".kt": "kotlin", - ".l": "ocaml", - ".ls": "livescript", - ".md": "markdown", - ".mjs": "javascript", - ".mli": "ocaml", - ".mll": "ocaml", - ".mly": "ocaml", - ".patch": "diff", - ".pl": "perl", - ".pm": "perl", - ".ps1": "powershell", - ".psd1": "powershell", - ".psm1": "powershell", - ".py": "python", - ".pyw": "python", - ".rb": "ruby", - ".rs": "rust", - ".scpt": "applescript", - ".scptd": "applescript", - ".sh": "bash", - ".tcsh": "shell", - ".ts": "typescript", - ".tsx": "typescript", - ".txt": "plaintext", - ".vb": "vbnet", - ".vbs": "vbscript", - ".yml": "yaml", - ".zsh": "shell", + lexer := lexers.Match(fileName) + if lexer == nil { + lexer = lexers.Fallback } -) -// NewContext loads highlight map -func NewContext() { - keys := setting.Cfg.Section("highlight.mapping").Keys() - for i := range keys { - highlightMapping[keys[i].Name()] = keys[i].Value() + iterator, err := lexer.Tokenise(nil, string(code)) + if err != nil { + log.Error("Can't tokenize code: %v", err) + return code } + // style not used for live site but need to pass something + err = formatter.Format(htmlw, styles.GitHub, iterator) + if err != nil { + log.Error("Can't format code: %v", err) + return code + } + + htmlw.Flush() + return htmlbuf.String() } -// FileNameToHighlightClass returns the best match for highlight class name -// based on the rule of highlight.js. -func FileNameToHighlightClass(fname string) string { - fname = strings.ToLower(fname) - if ignoreFileNames[fname] { - return "nohighlight" +// File returns map with line lumbers and HTML version of code with chroma syntax highlighting classes +func File(numLines int, fileName string, code []byte) map[int]string { + NewContext() + + if len(code) > sizeLimit { + return plainText(string(code), numLines) + } + formatter := html.New(html.WithClasses(true), + html.WithLineNumbers(false), + html.PreventSurroundingPre(true), + ) + + if formatter == nil { + log.Error("Couldn't create chroma formatter") + return plainText(string(code), numLines) + } + + htmlbuf := bytes.Buffer{} + htmlw := bufio.NewWriter(&htmlbuf) + + if val, ok := highlightMapping[filepath.Ext(fileName)]; ok { + fileName = "test." + val + } + + lexer := lexers.Match(fileName) + if lexer == nil { + lexer = lexers.Analyse(string(code)) + if lexer == nil { + lexer = lexers.Fallback + } } - if name, ok := highlightFileNames[fname]; ok { - return name + iterator, err := lexer.Tokenise(nil, string(code)) + if err != nil { + log.Error("Can't tokenize code: %v", err) + return plainText(string(code), numLines) } - ext := path.Ext(fname) - if _, ok := highlightExts[ext]; ok { - return ext[1:] + err = formatter.Format(htmlw, styles.GitHub, iterator) + if err != nil { + log.Error("Can't format code: %v", err) + return plainText(string(code), numLines) } - name, ok := highlightMapping[ext] - if ok { - return name + htmlw.Flush() + m := make(map[int]string, numLines) + for k, v := range strings.SplitN(htmlbuf.String(), "\n", numLines) { + line := k + 1 + m[line] = string(v) } + return m +} - return "" +// return unhiglighted map +func plainText(code string, numLines int) map[int]string { + m := make(map[int]string, numLines) + for k, v := range strings.SplitN(string(code), "\n", numLines) { + line := k + 1 + m[line] = string(v) + } + return m } diff --git a/modules/indexer/code/search.go b/modules/indexer/code/search.go index ca57b3ff88..29ed416541 100644 --- a/modules/indexer/code/search.go +++ b/modules/indexer/code/search.go @@ -6,8 +6,6 @@ package code import ( "bytes" - "html" - gotemplate "html/template" "strings" "code.gitea.io/gitea/modules/highlight" @@ -23,9 +21,8 @@ type Result struct { UpdatedUnix timeutil.TimeStamp Language string Color string - HighlightClass string LineNumbers []int - FormattedLines gotemplate.HTML + FormattedLines string } func indices(content string, selectionStartIndex, selectionEndIndex int) (int, int) { @@ -80,19 +77,13 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro openActiveIndex := util.Max(result.StartIndex-index, 0) closeActiveIndex := util.Min(result.EndIndex-index, len(line)) err = writeStrings(&formattedLinesBuffer, - `<li>`, - html.EscapeString(line[:openActiveIndex]), - `<span class='active'>`, - html.EscapeString(line[openActiveIndex:closeActiveIndex]), - `</span>`, - html.EscapeString(line[closeActiveIndex:]), - `</li>`, + line[:openActiveIndex], + line[openActiveIndex:closeActiveIndex], + line[closeActiveIndex:], ) } else { err = writeStrings(&formattedLinesBuffer, - `<li>`, - html.EscapeString(line), - `</li>`, + line, ) } if err != nil { @@ -109,9 +100,8 @@ func searchResult(result *SearchResult, startIndex, endIndex int) (*Result, erro UpdatedUnix: result.UpdatedUnix, Language: result.Language, Color: result.Color, - HighlightClass: highlight.FileNameToHighlightClass(result.Filename), LineNumbers: lineNumbers, - FormattedLines: gotemplate.HTML(formattedLinesBuffer.String()), + FormattedLines: highlight.Code(result.Filename, formattedLinesBuffer.String()), }, nil } diff --git a/modules/markup/markdown/markdown.go b/modules/markup/markdown/markdown.go index 128268bc88..9197dd2fe1 100644 --- a/modules/markup/markdown/markdown.go +++ b/modules/markup/markdown/markdown.go @@ -15,7 +15,9 @@ import ( "code.gitea.io/gitea/modules/setting" giteautil "code.gitea.io/gitea/modules/util" + chromahtml "github.com/alecthomas/chroma/formatters/html" "github.com/yuin/goldmark" + "github.com/yuin/goldmark-highlighting" meta "github.com/yuin/goldmark-meta" "github.com/yuin/goldmark/extension" "github.com/yuin/goldmark/parser" @@ -49,6 +51,30 @@ func render(body []byte, urlPrefix string, metas map[string]string, wikiMarkdown extension.TaskList, extension.DefinitionList, common.FootnoteExtension, + highlighting.NewHighlighting( + highlighting.WithFormatOptions( + chromahtml.WithClasses(true), + chromahtml.PreventSurroundingPre(true), + ), + highlighting.WithWrapperRenderer(func(w util.BufWriter, c highlighting.CodeBlockContext, entering bool) { + language, _ := c.Language() + if language == nil { + language = []byte("text") + } + if entering { + // include language-x class as part of commonmark spec + _, err := w.WriteString("<pre><code class=\"chroma language-" + string(language) + "\">") + if err != nil { + return + } + } else { + _, err := w.WriteString("</code></pre>") + if err != nil { + return + } + } + }), + ), meta.Meta, ), goldmark.WithParserOptions( diff --git a/modules/markup/sanitizer.go b/modules/markup/sanitizer.go index 1041d56a32..e5f6e75084 100644 --- a/modules/markup/sanitizer.go +++ b/modules/markup/sanitizer.go @@ -37,8 +37,8 @@ func NewSanitizer() { // ReplaceSanitizer replaces the current sanitizer to account for changes in settings func ReplaceSanitizer() { sanitizer.policy = bluemonday.UGCPolicy() - // We only want to allow HighlightJS specific classes for code blocks - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^language-[\w-]+$`)).OnElements("code") + // For Chroma markdown plugin + sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(chroma )?language-[\w-]+$`)).OnElements("code") // Checkboxes sanitizer.policy.AllowAttrs("type").Matching(regexp.MustCompile(`^checkbox$`)).OnElements("input") @@ -65,8 +65,8 @@ func ReplaceSanitizer() { // Allow classes for emojis sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img") - // Allow icons, checkboxes and emojis on span - sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(ui checkbox)|(ui checked checkbox)|(emoji))$`)).OnElements("span") + // Allow icons, checkboxes, emojis, and chroma syntax on span + sanitizer.policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(ui checkbox)|(ui checked checkbox)|(emoji))$|^([a-z][a-z0-9]{0,2})$`)).OnElements("span") // Allow generally safe attributes generalSafeAttrs := []string{"abbr", "accept", "accept-charset", diff --git a/modules/repofiles/diff_test.go b/modules/repofiles/diff_test.go index 5c09e180f3..d9e144be82 100644 --- a/modules/repofiles/diff_test.go +++ b/modules/repofiles/diff_test.go @@ -47,7 +47,8 @@ func TestGetDiffPreview(t *testing.T) { IsSubmodule: false, Sections: []*gitdiff.DiffSection{ { - Name: "", + FileName: "README.md", + Name: "", Lines: []*gitdiff.DiffLine{ { LeftIdx: 0, |