From 77e3dbed87581ef7d850f3781f170c241157fa32 Mon Sep 17 00:00:00 2001 From: mntn <85877297+mntn-xyz@users.noreply.github.com> Date: Fri, 10 Dec 2021 23:09:31 -0500 Subject: [PATCH] Add Chroma support for syntax highlighting (#263) --- README.md | 2 ++ config/config.go | 2 ++ config/default.go | 6 +++++ default-config.toml | 6 +++++ display/display.go | 13 ++++++++++ go.mod | 1 + go.sum | 20 ++++++++++++++ renderer/renderer.go | 62 +++++++++++++++++++++++++++++++++++++++++++- 8 files changed, 111 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 978e12a..9a42eb3 100644 --- a/README.md +++ b/README.md @@ -142,6 +142,7 @@ Features in *italics* are in the master branch, but not in the latest release. - So is subscribing to a page, to know when it changes - [x] Open non-text files in another application - [x] Ability to stream content instead of downloading it first +- [x] Highlighting of preformatted code blocks that list a language in the alt text - [ ] Stream support - [ ] Table of contents for pages - [ ] Search in pages with Ctrl-F @@ -162,6 +163,7 @@ Amfora ❤️ open source! - [progressbar](https://github.com/schollz/progressbar) - [go-humanize](https://github.com/dustin/go-humanize) - [gofeed](https://github.com/mmcdole/gofeed) +- [chroma](https://github.com/alecthomas/chroma) for source code syntax highlighting - [clipboard](https://github.com/atotto/clipboard) - [termenv](https://github.com/muesli/termenv) diff --git a/config/config.go b/config/config.go index f45e893..6c4bf1b 100644 --- a/config/config.go +++ b/config/config.go @@ -196,6 +196,8 @@ func Init() error { viper.SetDefault("a-general.search", "gemini://geminispace.info/search") viper.SetDefault("a-general.color", true) viper.SetDefault("a-general.ansi", true) + viper.SetDefault("a-general.highlight_code", true) + viper.SetDefault("a-general.highlight_style", "monokai") viper.SetDefault("a-general.bullets", true) viper.SetDefault("a-general.show_link", false) viper.SetDefault("a-general.left_margin", 0.15) diff --git a/config/default.go b/config/default.go index b6587d1..82a7b3e 100644 --- a/config/default.go +++ b/config/default.go @@ -58,6 +58,12 @@ color = true # Whether ANSI color codes from the page content should be rendered ansi = true +# Whether or not to support source code highlighting in preformatted blocks based on alt text +highlight_code = true + +# Which highlighting style to use (see https://xyproto.github.io/splash/docs/) +highlight_style = "monokai" + # Whether to replace list asterisks with unicode bullets bullets = true diff --git a/default-config.toml b/default-config.toml index 3369b92..7e034ed 100644 --- a/default-config.toml +++ b/default-config.toml @@ -55,6 +55,12 @@ color = true # Whether ANSI color codes from the page content should be rendered ansi = true +# Whether or not to support source code highlighting in preformatted blocks based on alt text +highlight_code = true + +# Which highlighting style to use (see https://xyproto.github.io/splash/docs/) +highlight_style = "monokai" + # Whether to replace list asterisks with unicode bullets bullets = true diff --git a/display/display.go b/display/display.go index 59f20db..f0fc1ea 100644 --- a/display/display.go +++ b/display/display.go @@ -15,6 +15,7 @@ import ( "github.com/makeworld-the-better-one/amfora/renderer" "github.com/makeworld-the-better-one/amfora/structs" "github.com/makeworld-the-better-one/go-gemini" + "github.com/muesli/termenv" "github.com/spf13/viper" ) @@ -60,6 +61,18 @@ var App = cview.NewApplication() func Init(version, commit, builtBy string) { aboutInit(version, commit, builtBy) + // Detect terminal colors for syntax highlighting + switch termenv.ColorProfile() { + case termenv.TrueColor: + renderer.TermColor = "terminal16m" + case termenv.ANSI256: + renderer.TermColor = "terminal256" + case termenv.ANSI: + renderer.TermColor = "terminal16" + case termenv.Ascii: + renderer.TermColor = "" + } + App.EnableMouse(false) App.SetRoot(layout, true) App.SetAfterResizeFunc(func(width int, height int) { diff --git a/go.mod b/go.mod index 4ca9052..9c63994 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( code.rocketnine.space/tslocum/cview v1.5.6-0.20210530175404-7e8817f20bdc + github.com/alecthomas/chroma v0.9.2 github.com/atotto/clipboard v0.1.4 github.com/dustin/go-humanize v1.0.0 github.com/fsnotify/fsnotify v1.4.9 // indirect diff --git a/go.sum b/go.sum index bec5ded..32656c4 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,15 @@ github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.1 h1:PSPBGne8NIUWw+/7vFBV+kG2J/5MOjbzc7154OaKCSE= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.9.2 h1:yU1sE2+TZbLIQPMk30SolL2Hn53SR/Pv750f7qZ/XMs= +github.com/alecthomas/chroma v0.9.2/go.mod h1:eMuEnpA18XbG/WhOWtCzJHS7WqEtDAI+HxdwoW0nVSk= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.2.4/go.mod h1:kQOmtJgV+Lb4aj+I2LEn40cbtawdWJ9Y8QLq+lElKxE= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/andybalholm/cascadia v1.1.0 h1:BuuO6sSfQNFRu1LppgbD25Hr2vLYW25JvxHs5zzsLTo= @@ -42,11 +51,15 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dlclark/regexp2 v1.4.0 h1:F1rxgk7p4uKjwIQxBs9oAXe5CqrXlCduYEJvrF4u93E= +github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= @@ -141,7 +154,9 @@ github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/makeworld-the-better-one/go-gemini v0.12.1 h1:cWHvCHL31Caq3Rm9elCFFoQeyrn92Kv7KummsVxCOFg= github.com/makeworld-the-better-one/go-gemini v0.12.1/go.mod h1:F+3x+R1xeYK90jMtBq+U+8Sh64r2dHleDZ/en3YgSmg= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -183,6 +198,7 @@ github.com/pelletier/go-toml v1.8.0 h1:Keo9qb7iRJs2voHvunFtuuYFsbWeOBh8/P9v/kVMF github.com/pelletier/go-toml v1.8.0/go.mod h1:D6yutnOGMveHEPV7VQOuvI/gXY61bv+9bAOTRnLElKs= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= @@ -207,6 +223,8 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/schollz/progressbar/v3 v3.8.0 h1:BKyefEMgFBDbo+JaeqHcm/9QdSj8qG8sUY+6UppGpnw= github.com/schollz/progressbar/v3 v3.8.0/go.mod h1:Y9mmL2knZj3LUaBDyBEzFdPrymIr08hnlFMZmfxwbx4= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= @@ -314,6 +332,8 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200413165638-669c56c373c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210223095934-7937bea0104d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/renderer/renderer.go b/renderer/renderer.go index 21ecf46..fe2ad62 100644 --- a/renderer/renderer.go +++ b/renderer/renderer.go @@ -5,6 +5,7 @@ package renderer import ( + "bytes" "fmt" urlPkg "net/url" "regexp" @@ -12,13 +13,25 @@ import ( "strings" "code.rocketnine.space/tslocum/cview" + "github.com/alecthomas/chroma/formatters" + "github.com/alecthomas/chroma/lexers" + "github.com/alecthomas/chroma/styles" "github.com/makeworld-the-better-one/amfora/config" "github.com/spf13/viper" ) +// Terminal color information, set during display initialization by display/display.go +var TermColor string + // Regex for identifying ANSI color codes var ansiRegex = regexp.MustCompile(`\x1b\[[0-9;]*m`) +// Regex for identifying possible language string, based on RFC 6838 and lexers used by Chroma +var langRegex = regexp.MustCompile(`^([a-zA-Z0-9]+/)?[a-zA-Z0-9]+([a-zA-Z0-9!_\#\$\&\-\^\.\+]+)*`) + +// Regex for removing trailing newline (without disturbing ANSI codes) from code formatted with Chroma +var trailingNewline = regexp.MustCompile(`(\r?\n)(?:\x1b\[[0-9;]*m)*$`) + // RenderANSI renders plain text pages containing ANSI codes. // Practically, it is used for the text/x-ansi. func RenderANSI(s string) string { @@ -315,11 +328,46 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { pre := false buf := "" // Block of regular or preformatted lines + // Language, formatter, and style for syntax highlighting + lang := "" + formatterName := TermColor + styleName := viper.GetString("a-general.highlight_style") + // processPre is for rendering preformatted blocks processPre := func() { + syntaxHighlighted := false + + // Perform syntax highlighting if language is set + if lang != "" { + style := styles.Get(styleName) + if style == nil { + style = styles.Fallback + } + formatter := formatters.Get(formatterName) + if formatter == nil { + formatter = formatters.Fallback + } + lexer := lexers.Get(lang) + if lexer == nil { + lexer = lexers.Fallback + } + + // Tokenize and format the text after stripping ANSI codes, replacing buffer if there are no errors + iterator, err := lexer.Tokenise(nil, ansiRegex.ReplaceAllString(buf, "")) + if err == nil { + formattedBuffer := new(bytes.Buffer) + if formatter.Format(formattedBuffer, style, iterator) == nil { + // Strip extra newline added by Chroma and replace buffer + buf = string(trailingNewline.ReplaceAll(formattedBuffer.Bytes(), []byte{})) + } + syntaxHighlighted = true + } + } + // Support ANSI color codes in preformatted blocks - see #59 - if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") { + // This will also execute if code highlighting was successful for this block + if viper.GetBool("a-general.color") && (viper.GetBool("a-general.ansi") || syntaxHighlighted) { buf = cview.TranslateANSI(buf) // The TranslateANSI function will reset the colors when it encounters // an ANSI reset code, injecting a full reset tag: [-:-:-] @@ -366,9 +414,21 @@ func RenderGemini(s string, width int, proxied bool) (string, []string) { // Don't add the current line with backticks processPre() + // Clear the language + lang = "" } else { // Not preformatted, regular text processRegular() + + if viper.GetBool("a-general.highlight_code") { + // Check for alt text indicating a language that Chroma can highlight + alt := strings.TrimSpace(strings.TrimPrefix(lines[i], "```")) + if matches := langRegex.FindStringSubmatch(alt); matches != nil { + if lexers.Get(matches[0]) != nil { + lang = matches[0] + } + } + } } buf = "" // Clear buffer for next block pre = !pre