prettify.go 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. // License: GPLv3 Copyright: 2022, Kovid Goyal, <kovid at kovidgoyal.net>
  2. package markup
  3. import (
  4. "fmt"
  5. "os"
  6. "path/filepath"
  7. "strings"
  8. "unicode"
  9. "kitty"
  10. "kitty/tools/utils"
  11. "kitty/tools/utils/style"
  12. )
  13. var _ = fmt.Print
  14. type Context struct {
  15. fmt_ctx style.Context
  16. Cyan, Green, Blue, Magenta, Red, BrightRed, Yellow, Italic, Bold, Dim, Title, Exe, Opt, Emph, Err, Code func(args ...interface{}) string
  17. Url func(string, string) string
  18. }
  19. var (
  20. fmt_ctx = style.Context{}
  21. )
  22. func New(allow_escape_codes bool) *Context {
  23. ans := Context{}
  24. ans.fmt_ctx.AllowEscapeCodes = allow_escape_codes
  25. fmt_ctx := &ans.fmt_ctx
  26. ans.Cyan = fmt_ctx.SprintFunc("fg=bright-cyan")
  27. ans.Red = fmt_ctx.SprintFunc("fg=red")
  28. ans.Magenta = fmt_ctx.SprintFunc("fg=magenta")
  29. ans.Green = fmt_ctx.SprintFunc("fg=green")
  30. ans.Blue = fmt_ctx.SprintFunc("fg=blue")
  31. ans.BrightRed = fmt_ctx.SprintFunc("fg=bright-red")
  32. ans.Yellow = fmt_ctx.SprintFunc("fg=bright-yellow")
  33. ans.Italic = fmt_ctx.SprintFunc("italic")
  34. ans.Bold = fmt_ctx.SprintFunc("bold")
  35. ans.Dim = fmt_ctx.SprintFunc("dim")
  36. ans.Title = fmt_ctx.SprintFunc("bold fg=blue")
  37. ans.Exe = fmt_ctx.SprintFunc("bold fg=bright-yellow")
  38. ans.Opt = ans.Green
  39. ans.Emph = ans.BrightRed
  40. ans.Err = fmt_ctx.SprintFunc("bold fg=bright-red")
  41. ans.Code = ans.Cyan
  42. ans.Url = fmt_ctx.UrlFunc("u=curly uc=cyan")
  43. return &ans
  44. }
  45. func Remove_backslash_escapes(text string) string {
  46. // see https://docutils.sourceforge.io/docs/ref/rst/restructuredtext.html#escaping-mechanism
  47. out := strings.Builder{}
  48. prev_was_slash := false
  49. out.Grow(len(text))
  50. for _, ch := range text {
  51. if prev_was_slash {
  52. if !unicode.IsSpace(ch) {
  53. out.WriteRune(ch)
  54. }
  55. prev_was_slash = false
  56. } else {
  57. if ch == '\\' {
  58. prev_was_slash = true
  59. } else {
  60. out.WriteRune(ch)
  61. }
  62. }
  63. }
  64. return out.String()
  65. }
  66. func ReplaceAllRSTRoles(str string, repl func(Rst_format_match) string) string {
  67. var m Rst_format_match
  68. rf := func(full_match string, groupdict map[string]utils.SubMatch) string {
  69. m.Payload = groupdict["payload"].Text
  70. m.Role = groupdict["role"].Text
  71. return repl(m)
  72. }
  73. return utils.ReplaceAll(utils.MustCompile(":(?P<role>[a-z]+):(?:(?:`(?P<payload>[^`]+)`)|(?:'(?P<payload>[^']+)'))"), str, rf)
  74. }
  75. func (self *Context) hyperlink_for_url(url string, text string) string {
  76. return self.Url(url, text)
  77. }
  78. func (self *Context) hyperlink_for_path(path string, text string) string {
  79. if !fmt_ctx.AllowEscapeCodes {
  80. return text
  81. }
  82. path = strings.ReplaceAll(utils.Abspath(path), string(os.PathSeparator), "/")
  83. fi, err := os.Stat(path)
  84. if err == nil && fi.IsDir() {
  85. path = strings.TrimSuffix(path, "/") + "/"
  86. }
  87. host := utils.Hostname()
  88. url := "file://" + host + path
  89. return self.hyperlink_for_url(url, text)
  90. }
  91. func Text_and_target(x string) (text string, target string) {
  92. parts := strings.SplitN(x, "<", 2)
  93. text = strings.TrimSpace(parts[0])
  94. target = strings.TrimRight(parts[len(parts)-1], ">")
  95. return
  96. }
  97. type Rst_format_match struct {
  98. Role, Payload string
  99. }
  100. func (self *Context) link(x string) string {
  101. text, url := Text_and_target(x)
  102. return self.hyperlink_for_url(url, text)
  103. }
  104. func (self *Context) ref_hyperlink(x string, prefix string) string {
  105. text, target := Text_and_target(x)
  106. url := "kitty+doc://" + utils.Hostname() + "/#ref=" + prefix + target
  107. text = ReplaceAllRSTRoles(text, func(group Rst_format_match) string {
  108. return group.Payload
  109. })
  110. return self.hyperlink_for_url(url, text)
  111. }
  112. func (self *Context) Prettify(text string) string {
  113. return ReplaceAllRSTRoles(text, func(group Rst_format_match) string {
  114. val := group.Payload
  115. switch group.Role {
  116. case "file":
  117. if val == "kitty.conf" && self.fmt_ctx.AllowEscapeCodes {
  118. path := filepath.Join(utils.ConfigDir(), val)
  119. val = self.hyperlink_for_path(path, val)
  120. }
  121. return self.Italic(val)
  122. case "env", "envvar":
  123. return self.ref_hyperlink(val, "envvar-")
  124. case "doc":
  125. text, target := Text_and_target(val)
  126. no_title := text == target
  127. target = strings.Trim(target, "/")
  128. if title, ok := kitty.DocTitleMap[target]; ok && no_title {
  129. val = title + " <" + target + ">"
  130. } else {
  131. val = text + " <" + target + ">"
  132. }
  133. return self.ref_hyperlink(val, "doc-")
  134. case "iss":
  135. return self.ref_hyperlink(val, "issues-")
  136. case "pull":
  137. return self.ref_hyperlink(val, "pull-")
  138. case "disc":
  139. return self.ref_hyperlink(val, "discussions-")
  140. case "ref":
  141. return self.ref_hyperlink(val, "")
  142. case "ac":
  143. return self.ref_hyperlink(val, "action-")
  144. case "term":
  145. return self.ref_hyperlink(val, "term-")
  146. case "code":
  147. return self.Code(Remove_backslash_escapes(val))
  148. case "link":
  149. return self.link(val)
  150. case "option":
  151. idx := strings.LastIndex(val, "--")
  152. if idx < 0 {
  153. idx = strings.Index(val, "-")
  154. }
  155. if idx > -1 {
  156. val = strings.TrimSuffix(val[idx:], ">")
  157. }
  158. return self.Bold(val)
  159. case "opt":
  160. return self.Bold(val)
  161. case "yellow":
  162. return self.Yellow(val)
  163. case "blue":
  164. return self.Blue(val)
  165. case "green":
  166. return self.Green(val)
  167. case "cyan":
  168. return self.Cyan(val)
  169. case "magenta":
  170. return self.Magenta(val)
  171. case "emph":
  172. return self.Italic(val)
  173. default:
  174. return val
  175. }
  176. })
  177. }
  178. func (self *Context) SetAllowEscapeCodes(allow_escape_codes bool) {
  179. self.fmt_ctx.AllowEscapeCodes = allow_escape_codes
  180. }
  181. func (self *Context) EscapeCodesAllowed() bool {
  182. return self.fmt_ctx.AllowEscapeCodes
  183. }