// License: GPLv3 Copyright: 2022, Kovid Goyal, 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(utils.MustCompile(":(?P[a-z]+):(?:(?:`(?P[^`]+)`)|(?:'(?P[^']+)'))"), 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) no_title := text == target target = strings.Trim(target, "/") if title, ok := kitty.DocTitleMap[target]; ok && no_title { val = title + " <" + target + ">" } else { val = text + " <" + 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 }