kitty/tools/cli/markup/prettify.go
2023-03-03 15:20:35 +05:30

201 lines
5.2 KiB
Go

// License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
package markup
import (
"fmt"
"os"
"path/filepath"
"strings"
"unicode"
"kitty"
"kitty/tools/utils"
"kitty/tools/utils/style"
)
var _ = fmt.Print
type Context struct {
fmt_ctx style.Context
Cyan, Green, Blue, BrightRed, Yellow, Italic, Bold, Dim, Title, Exe, Opt, Emph, Err, Code func(args ...interface{}) string
Url func(string, string) string
}
var (
fmt_ctx = style.Context{}
)
func New(allow_escape_codes bool) *Context {
ans := Context{}
ans.fmt_ctx.AllowEscapeCodes = allow_escape_codes
fmt_ctx := &ans.fmt_ctx
ans.Cyan = fmt_ctx.SprintFunc("fg=bright-cyan")
ans.Green = fmt_ctx.SprintFunc("fg=green")
ans.Blue = fmt_ctx.SprintFunc("fg=blue")
ans.BrightRed = fmt_ctx.SprintFunc("fg=bright-red")
ans.Yellow = fmt_ctx.SprintFunc("fg=bright-yellow")
ans.Italic = fmt_ctx.SprintFunc("italic")
ans.Bold = fmt_ctx.SprintFunc("bold")
ans.Dim = fmt_ctx.SprintFunc("dim")
ans.Title = fmt_ctx.SprintFunc("bold fg=blue")
ans.Exe = fmt_ctx.SprintFunc("bold fg=bright-yellow")
ans.Opt = ans.Green
ans.Emph = ans.BrightRed
ans.Err = fmt_ctx.SprintFunc("bold fg=bright-red")
ans.Code = ans.Cyan
ans.Url = fmt_ctx.UrlFunc("u=curly uc=cyan")
return &ans
}
func remove_backslash_escapes(text string) string {
// see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#escaping-mechanism
out := strings.Builder{}
prev_was_slash := false
out.Grow(len(text))
for _, ch := range text {
if prev_was_slash {
if !unicode.IsSpace(ch) {
out.WriteRune(ch)
}
prev_was_slash = false
} else {
if ch == '\\' {
prev_was_slash = true
} else {
out.WriteRune(ch)
}
}
}
return out.String()
}
func replace_all_rst_roles(str string, repl func(rst_format_match) string) string {
var m rst_format_match
rf := func(full_match string, groupdict map[string]utils.SubMatch) string {
m.payload = groupdict["payload"].Text
m.role = groupdict["role"].Text
return repl(m)
}
return utils.ReplaceAll(":(?P<role>[a-z]+):(?:(?:`(?P<payload>[^`]+)`)|(?:'(?P<payload>[^']+)'))", str, rf)
}
func (self *Context) hyperlink_for_url(url string, text string) string {
return self.Url(url, text)
}
func (self *Context) hyperlink_for_path(path string, text string) string {
if !fmt_ctx.AllowEscapeCodes {
return text
}
path = strings.ReplaceAll(utils.Abspath(path), string(os.PathSeparator), "/")
fi, err := os.Stat(path)
if err == nil && fi.IsDir() {
path = strings.TrimSuffix(path, "/") + "/"
}
host := utils.Hostname()
url := "file://" + host + path
return self.hyperlink_for_url(url, text)
}
func text_and_target(x string) (text string, target string) {
parts := strings.SplitN(x, "<", 2)
text = strings.TrimSpace(parts[0])
target = strings.TrimRight(parts[len(parts)-1], ">")
return
}
type rst_format_match struct {
role, payload string
}
func (self *Context) link(x string) string {
text, url := text_and_target(x)
return self.hyperlink_for_url(url, text)
}
func (self *Context) ref_hyperlink(x string, prefix string) string {
text, target := text_and_target(x)
url := "kitty+doc://" + utils.Hostname() + "/#ref=" + prefix + target
text = replace_all_rst_roles(text, func(group rst_format_match) string {
return group.payload
})
return self.hyperlink_for_url(url, text)
}
func (self *Context) Prettify(text string) string {
return replace_all_rst_roles(text, func(group rst_format_match) string {
val := group.payload
switch group.role {
case "file":
if val == "kitty.conf" && self.fmt_ctx.AllowEscapeCodes {
path := filepath.Join(utils.ConfigDir(), val)
val = self.hyperlink_for_path(path, val)
}
return self.Italic(val)
case "env", "envvar":
return self.ref_hyperlink(val, "envvar-")
case "doc":
text, target := text_and_target(val)
if text == target {
target = strings.Trim(target, "/")
if title, ok := kitty.DocTitleMap[target]; ok {
val = title + " <" + target + ">"
}
}
return self.ref_hyperlink(val, "doc-")
case "iss":
return self.ref_hyperlink(val, "issues-")
case "pull":
return self.ref_hyperlink(val, "pull-")
case "disc":
return self.ref_hyperlink(val, "discussions-")
case "ref":
return self.ref_hyperlink(val, "")
case "ac":
return self.ref_hyperlink(val, "action-")
case "term":
return self.ref_hyperlink(val, "term-")
case "code":
return self.Code(remove_backslash_escapes(val))
case "link":
return self.link(val)
case "option":
idx := strings.LastIndex(val, "--")
if idx < 0 {
idx = strings.Index(val, "-")
}
if idx > -1 {
val = strings.TrimSuffix(val[idx:], ">")
}
return self.Bold(val)
case "opt":
return self.Bold(val)
case "yellow":
return self.Yellow(val)
case "blue":
return self.Blue(val)
case "green":
return self.Green(val)
case "cyan":
return self.Cyan(val)
case "emph":
return self.Italic(val)
default:
return val
}
})
}
func (self *Context) SetAllowEscapeCodes(allow_escape_codes bool) {
self.fmt_ctx.AllowEscapeCodes = allow_escape_codes
}
func (self *Context) EscapeCodesAllowed() bool {
return self.fmt_ctx.AllowEscapeCodes
}