mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-11-22 07:23:05 +03:00
Add Chroma support for syntax highlighting (#263)
This commit is contained in:
parent
e62da93e57
commit
77e3dbed87
@ -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 <kbd>Ctrl-F</kbd>
|
||||
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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) {
|
||||
|
1
go.mod
1
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
|
||||
|
20
go.sum
20
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=
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user