textbuilder.go 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. // Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
  2. // Use of this source code is governed by a MIT license that can
  3. // be found in the LICENSE file.
  4. package termui
  5. import (
  6. "regexp"
  7. "strings"
  8. "github.com/mitchellh/go-wordwrap"
  9. )
  10. // TextBuilder is a minimal interface to produce text []Cell using specific syntax (markdown).
  11. type TextBuilder interface {
  12. Build(s string, fg, bg Attribute) []Cell
  13. }
  14. // DefaultTxBuilder is set to be MarkdownTxBuilder.
  15. var DefaultTxBuilder = NewMarkdownTxBuilder()
  16. // MarkdownTxBuilder implements TextBuilder interface, using markdown syntax.
  17. type MarkdownTxBuilder struct {
  18. baseFg Attribute
  19. baseBg Attribute
  20. plainTx []rune
  21. markers []marker
  22. }
  23. type marker struct {
  24. st int
  25. ed int
  26. fg Attribute
  27. bg Attribute
  28. }
  29. var colorMap = map[string]Attribute{
  30. "red": ColorRed,
  31. "blue": ColorBlue,
  32. "black": ColorBlack,
  33. "cyan": ColorCyan,
  34. "yellow": ColorYellow,
  35. "white": ColorWhite,
  36. "default": ColorDefault,
  37. "green": ColorGreen,
  38. "magenta": ColorMagenta,
  39. }
  40. var attrMap = map[string]Attribute{
  41. "bold": AttrBold,
  42. "underline": AttrUnderline,
  43. "reverse": AttrReverse,
  44. }
  45. func rmSpc(s string) string {
  46. reg := regexp.MustCompile(`\s+`)
  47. return reg.ReplaceAllString(s, "")
  48. }
  49. // readAttr translates strings like `fg-red,fg-bold,bg-white` to fg and bg Attribute
  50. func (mtb MarkdownTxBuilder) readAttr(s string) (Attribute, Attribute) {
  51. fg := mtb.baseFg
  52. bg := mtb.baseBg
  53. updateAttr := func(a Attribute, attrs []string) Attribute {
  54. for _, s := range attrs {
  55. // replace the color
  56. if c, ok := colorMap[s]; ok {
  57. a &= 0xFF00 // erase clr 0 ~ 8 bits
  58. a |= c // set clr
  59. }
  60. // add attrs
  61. if c, ok := attrMap[s]; ok {
  62. a |= c
  63. }
  64. }
  65. return a
  66. }
  67. ss := strings.Split(s, ",")
  68. fgs := []string{}
  69. bgs := []string{}
  70. for _, v := range ss {
  71. subs := strings.Split(v, "-")
  72. if len(subs) > 1 {
  73. if subs[0] == "fg" {
  74. fgs = append(fgs, subs[1])
  75. }
  76. if subs[0] == "bg" {
  77. bgs = append(bgs, subs[1])
  78. }
  79. }
  80. }
  81. fg = updateAttr(fg, fgs)
  82. bg = updateAttr(bg, bgs)
  83. return fg, bg
  84. }
  85. func (mtb *MarkdownTxBuilder) reset() {
  86. mtb.plainTx = []rune{}
  87. mtb.markers = []marker{}
  88. }
  89. // parse streams and parses text into normalized text and render sequence.
  90. func (mtb *MarkdownTxBuilder) parse(str string) {
  91. rs := str2runes(str)
  92. normTx := []rune{}
  93. square := []rune{}
  94. brackt := []rune{}
  95. accSquare := false
  96. accBrackt := false
  97. cntSquare := 0
  98. reset := func() {
  99. square = []rune{}
  100. brackt = []rune{}
  101. accSquare = false
  102. accBrackt = false
  103. cntSquare = 0
  104. }
  105. // pipe stacks into normTx and clear
  106. rollback := func() {
  107. normTx = append(normTx, square...)
  108. normTx = append(normTx, brackt...)
  109. reset()
  110. }
  111. // chop first and last
  112. chop := func(s []rune) []rune {
  113. return s[1 : len(s)-1]
  114. }
  115. for i, r := range rs {
  116. switch {
  117. // stacking brackt
  118. case accBrackt:
  119. brackt = append(brackt, r)
  120. if ')' == r {
  121. fg, bg := mtb.readAttr(string(chop(brackt)))
  122. st := len(normTx)
  123. ed := len(normTx) + len(square) - 2
  124. mtb.markers = append(mtb.markers, marker{st, ed, fg, bg})
  125. normTx = append(normTx, chop(square)...)
  126. reset()
  127. } else if i+1 == len(rs) {
  128. rollback()
  129. }
  130. // stacking square
  131. case accSquare:
  132. switch {
  133. // squares closed and followed by a '('
  134. case cntSquare == 0 && '(' == r:
  135. accBrackt = true
  136. brackt = append(brackt, '(')
  137. // squares closed but not followed by a '('
  138. case cntSquare == 0:
  139. rollback()
  140. if '[' == r {
  141. accSquare = true
  142. cntSquare = 1
  143. brackt = append(brackt, '[')
  144. } else {
  145. normTx = append(normTx, r)
  146. }
  147. // hit the end
  148. case i+1 == len(rs):
  149. square = append(square, r)
  150. rollback()
  151. case '[' == r:
  152. cntSquare++
  153. square = append(square, '[')
  154. case ']' == r:
  155. cntSquare--
  156. square = append(square, ']')
  157. // normal char
  158. default:
  159. square = append(square, r)
  160. }
  161. // stacking normTx
  162. default:
  163. if '[' == r {
  164. accSquare = true
  165. cntSquare = 1
  166. square = append(square, '[')
  167. } else {
  168. normTx = append(normTx, r)
  169. }
  170. }
  171. }
  172. mtb.plainTx = normTx
  173. }
  174. func wrapTx(cs []Cell, wl int) []Cell {
  175. tmpCell := make([]Cell, len(cs))
  176. copy(tmpCell, cs)
  177. // get the plaintext
  178. plain := CellsToStr(cs)
  179. // wrap
  180. plainWrapped := wordwrap.WrapString(plain, uint(wl))
  181. // find differences and insert
  182. finalCell := tmpCell // finalcell will get the inserts and is what is returned
  183. plainRune := []rune(plain)
  184. plainWrappedRune := []rune(plainWrapped)
  185. trigger := "go"
  186. plainRuneNew := plainRune
  187. for trigger != "stop" {
  188. plainRune = plainRuneNew
  189. for i := range plainRune {
  190. if plainRune[i] == plainWrappedRune[i] {
  191. trigger = "stop"
  192. } else if plainRune[i] != plainWrappedRune[i] && plainWrappedRune[i] == 10 {
  193. trigger = "go"
  194. cell := Cell{10, 0, 0}
  195. j := i - 0
  196. // insert a cell into the []Cell in correct position
  197. tmpCell[i] = cell
  198. // insert the newline into plain so we avoid indexing errors
  199. plainRuneNew = append(plainRune, 10)
  200. copy(plainRuneNew[j+1:], plainRuneNew[j:])
  201. plainRuneNew[j] = plainWrappedRune[j]
  202. // restart the inner for loop until plain and plain wrapped are
  203. // the same; yeah, it's inefficient, but the text amounts
  204. // should be small
  205. break
  206. } else if plainRune[i] != plainWrappedRune[i] &&
  207. plainWrappedRune[i-1] == 10 && // if the prior rune is a newline
  208. plainRune[i] == 32 { // and this rune is a space
  209. trigger = "go"
  210. // need to delete plainRune[i] because it gets rid of an extra
  211. // space
  212. plainRuneNew = append(plainRune[:i], plainRune[i+1:]...)
  213. break
  214. } else {
  215. trigger = "stop" // stops the outer for loop
  216. }
  217. }
  218. }
  219. finalCell = tmpCell
  220. return finalCell
  221. }
  222. // Build implements TextBuilder interface.
  223. func (mtb MarkdownTxBuilder) Build(s string, fg, bg Attribute) []Cell {
  224. mtb.baseFg = fg
  225. mtb.baseBg = bg
  226. mtb.reset()
  227. mtb.parse(s)
  228. cs := make([]Cell, len(mtb.plainTx))
  229. for i := range cs {
  230. cs[i] = Cell{Ch: mtb.plainTx[i], Fg: fg, Bg: bg}
  231. }
  232. for _, mrk := range mtb.markers {
  233. for i := mrk.st; i < mrk.ed; i++ {
  234. cs[i].Fg = mrk.fg
  235. cs[i].Bg = mrk.bg
  236. }
  237. }
  238. return cs
  239. }
  240. // NewMarkdownTxBuilder returns a TextBuilder employing markdown syntax.
  241. func NewMarkdownTxBuilder() TextBuilder {
  242. return MarkdownTxBuilder{}
  243. }