style.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. package lipgloss
  2. import (
  3. "strings"
  4. "unicode"
  5. "github.com/muesli/reflow/truncate"
  6. "github.com/muesli/reflow/wordwrap"
  7. "github.com/muesli/reflow/wrap"
  8. "github.com/muesli/termenv"
  9. )
  10. // Property for a key.
  11. type propKey int
  12. // Available properties.
  13. const (
  14. boldKey propKey = iota
  15. italicKey
  16. underlineKey
  17. strikethroughKey
  18. reverseKey
  19. blinkKey
  20. faintKey
  21. foregroundKey
  22. backgroundKey
  23. widthKey
  24. heightKey
  25. alignHorizontalKey
  26. alignVerticalKey
  27. // Padding.
  28. paddingTopKey
  29. paddingRightKey
  30. paddingBottomKey
  31. paddingLeftKey
  32. colorWhitespaceKey
  33. // Margins.
  34. marginTopKey
  35. marginRightKey
  36. marginBottomKey
  37. marginLeftKey
  38. marginBackgroundKey
  39. // Border runes.
  40. borderStyleKey
  41. // Border edges.
  42. borderTopKey
  43. borderRightKey
  44. borderBottomKey
  45. borderLeftKey
  46. // Border foreground colors.
  47. borderTopForegroundKey
  48. borderRightForegroundKey
  49. borderBottomForegroundKey
  50. borderLeftForegroundKey
  51. // Border background colors.
  52. borderTopBackgroundKey
  53. borderRightBackgroundKey
  54. borderBottomBackgroundKey
  55. borderLeftBackgroundKey
  56. inlineKey
  57. maxWidthKey
  58. maxHeightKey
  59. underlineSpacesKey
  60. strikethroughSpacesKey
  61. )
  62. // A set of properties.
  63. type rules map[propKey]interface{}
  64. // NewStyle returns a new, empty Style. While it's syntactic sugar for the
  65. // Style{} primitive, it's recommended to use this function for creating styles
  66. // in case the underlying implementation changes. It takes an optional string
  67. // value to be set as the underlying string value for this style.
  68. func NewStyle() Style {
  69. return renderer.NewStyle()
  70. }
  71. // NewStyle returns a new, empty Style. While it's syntactic sugar for the
  72. // Style{} primitive, it's recommended to use this function for creating styles
  73. // in case the underlying implementation changes. It takes an optional string
  74. // value to be set as the underlying string value for this style.
  75. func (r *Renderer) NewStyle() Style {
  76. s := Style{r: r}
  77. return s
  78. }
  79. // Style contains a set of rules that comprise a style as a whole.
  80. type Style struct {
  81. r *Renderer
  82. rules map[propKey]interface{}
  83. value string
  84. }
  85. // joinString joins a list of strings into a single string separated with a
  86. // space.
  87. func joinString(strs ...string) string {
  88. return strings.Join(strs, " ")
  89. }
  90. // SetString sets the underlying string value for this style. To render once
  91. // the underlying string is set, use the Style.String. This method is
  92. // a convenience for cases when having a stringer implementation is handy, such
  93. // as when using fmt.Sprintf. You can also simply define a style and render out
  94. // strings directly with Style.Render.
  95. func (s Style) SetString(strs ...string) Style {
  96. s.value = joinString(strs...)
  97. return s
  98. }
  99. // Value returns the raw, unformatted, underlying string value for this style.
  100. func (s Style) Value() string {
  101. return s.value
  102. }
  103. // String implements stringer for a Style, returning the rendered result based
  104. // on the rules in this style. An underlying string value must be set with
  105. // Style.SetString prior to using this method.
  106. func (s Style) String() string {
  107. return s.Render()
  108. }
  109. // Copy returns a copy of this style, including any underlying string values.
  110. func (s Style) Copy() Style {
  111. o := NewStyle()
  112. o.init()
  113. for k, v := range s.rules {
  114. o.rules[k] = v
  115. }
  116. o.r = s.r
  117. o.value = s.value
  118. return o
  119. }
  120. // Inherit overlays the style in the argument onto this style by copying each explicitly
  121. // set value from the argument style onto this style if it is not already explicitly set.
  122. // Existing set values are kept intact and not overwritten.
  123. //
  124. // Margins, padding, and underlying string values are not inherited.
  125. func (s Style) Inherit(i Style) Style {
  126. s.init()
  127. for k, v := range i.rules {
  128. switch k {
  129. case marginTopKey, marginRightKey, marginBottomKey, marginLeftKey:
  130. // Margins are not inherited
  131. continue
  132. case paddingTopKey, paddingRightKey, paddingBottomKey, paddingLeftKey:
  133. // Padding is not inherited
  134. continue
  135. case backgroundKey:
  136. // The margins also inherit the background color
  137. if !s.isSet(marginBackgroundKey) && !i.isSet(marginBackgroundKey) {
  138. s.rules[marginBackgroundKey] = v
  139. }
  140. }
  141. if _, exists := s.rules[k]; exists {
  142. continue
  143. }
  144. s.rules[k] = v
  145. }
  146. return s
  147. }
  148. // Render applies the defined style formatting to a given string.
  149. func (s Style) Render(strs ...string) string {
  150. if s.r == nil {
  151. s.r = renderer
  152. }
  153. if s.value != "" {
  154. strs = append([]string{s.value}, strs...)
  155. }
  156. var (
  157. str = joinString(strs...)
  158. te = s.r.ColorProfile().String()
  159. teSpace = s.r.ColorProfile().String()
  160. teWhitespace = s.r.ColorProfile().String()
  161. bold = s.getAsBool(boldKey, false)
  162. italic = s.getAsBool(italicKey, false)
  163. underline = s.getAsBool(underlineKey, false)
  164. strikethrough = s.getAsBool(strikethroughKey, false)
  165. reverse = s.getAsBool(reverseKey, false)
  166. blink = s.getAsBool(blinkKey, false)
  167. faint = s.getAsBool(faintKey, false)
  168. fg = s.getAsColor(foregroundKey)
  169. bg = s.getAsColor(backgroundKey)
  170. width = s.getAsInt(widthKey)
  171. height = s.getAsInt(heightKey)
  172. horizontalAlign = s.getAsPosition(alignHorizontalKey)
  173. verticalAlign = s.getAsPosition(alignVerticalKey)
  174. topPadding = s.getAsInt(paddingTopKey)
  175. rightPadding = s.getAsInt(paddingRightKey)
  176. bottomPadding = s.getAsInt(paddingBottomKey)
  177. leftPadding = s.getAsInt(paddingLeftKey)
  178. colorWhitespace = s.getAsBool(colorWhitespaceKey, true)
  179. inline = s.getAsBool(inlineKey, false)
  180. maxWidth = s.getAsInt(maxWidthKey)
  181. maxHeight = s.getAsInt(maxHeightKey)
  182. underlineSpaces = underline && s.getAsBool(underlineSpacesKey, true)
  183. strikethroughSpaces = strikethrough && s.getAsBool(strikethroughSpacesKey, true)
  184. // Do we need to style whitespace (padding and space outside
  185. // paragraphs) separately?
  186. styleWhitespace = reverse
  187. // Do we need to style spaces separately?
  188. useSpaceStyler = underlineSpaces || strikethroughSpaces
  189. )
  190. if len(s.rules) == 0 {
  191. return str
  192. }
  193. // Enable support for ANSI on the legacy Windows cmd.exe console. This is a
  194. // no-op on non-Windows systems and on Windows runs only once.
  195. enableLegacyWindowsANSI()
  196. if bold {
  197. te = te.Bold()
  198. }
  199. if italic {
  200. te = te.Italic()
  201. }
  202. if underline {
  203. te = te.Underline()
  204. }
  205. if reverse {
  206. if reverse {
  207. teWhitespace = teWhitespace.Reverse()
  208. }
  209. te = te.Reverse()
  210. }
  211. if blink {
  212. te = te.Blink()
  213. }
  214. if faint {
  215. te = te.Faint()
  216. }
  217. if fg != noColor {
  218. te = te.Foreground(fg.color(s.r))
  219. if styleWhitespace {
  220. teWhitespace = teWhitespace.Foreground(fg.color(s.r))
  221. }
  222. if useSpaceStyler {
  223. teSpace = teSpace.Foreground(fg.color(s.r))
  224. }
  225. }
  226. if bg != noColor {
  227. te = te.Background(bg.color(s.r))
  228. if colorWhitespace {
  229. teWhitespace = teWhitespace.Background(bg.color(s.r))
  230. }
  231. if useSpaceStyler {
  232. teSpace = teSpace.Background(bg.color(s.r))
  233. }
  234. }
  235. if underline {
  236. te = te.Underline()
  237. }
  238. if strikethrough {
  239. te = te.CrossOut()
  240. }
  241. if underlineSpaces {
  242. teSpace = teSpace.Underline()
  243. }
  244. if strikethroughSpaces {
  245. teSpace = teSpace.CrossOut()
  246. }
  247. // Strip newlines in single line mode
  248. if inline {
  249. str = strings.ReplaceAll(str, "\n", "")
  250. }
  251. // Word wrap
  252. if !inline && width > 0 {
  253. wrapAt := width - leftPadding - rightPadding
  254. str = wordwrap.String(str, wrapAt)
  255. str = wrap.String(str, wrapAt) // force-wrap long strings
  256. }
  257. // Render core text
  258. {
  259. var b strings.Builder
  260. l := strings.Split(str, "\n")
  261. for i := range l {
  262. if useSpaceStyler {
  263. // Look for spaces and apply a different styler
  264. for _, r := range l[i] {
  265. if unicode.IsSpace(r) {
  266. b.WriteString(teSpace.Styled(string(r)))
  267. continue
  268. }
  269. b.WriteString(te.Styled(string(r)))
  270. }
  271. } else {
  272. b.WriteString(te.Styled(l[i]))
  273. }
  274. if i != len(l)-1 {
  275. b.WriteRune('\n')
  276. }
  277. }
  278. str = b.String()
  279. }
  280. // Padding
  281. if !inline {
  282. if leftPadding > 0 {
  283. var st *termenv.Style
  284. if colorWhitespace || styleWhitespace {
  285. st = &teWhitespace
  286. }
  287. str = padLeft(str, leftPadding, st)
  288. }
  289. if rightPadding > 0 {
  290. var st *termenv.Style
  291. if colorWhitespace || styleWhitespace {
  292. st = &teWhitespace
  293. }
  294. str = padRight(str, rightPadding, st)
  295. }
  296. if topPadding > 0 {
  297. str = strings.Repeat("\n", topPadding) + str
  298. }
  299. if bottomPadding > 0 {
  300. str += strings.Repeat("\n", bottomPadding)
  301. }
  302. }
  303. // Height
  304. if height > 0 {
  305. str = alignTextVertical(str, verticalAlign, height, nil)
  306. }
  307. // Set alignment. This will also pad short lines with spaces so that all
  308. // lines are the same length, so we run it under a few different conditions
  309. // beyond alignment.
  310. {
  311. numLines := strings.Count(str, "\n")
  312. if !(numLines == 0 && width == 0) {
  313. var st *termenv.Style
  314. if colorWhitespace || styleWhitespace {
  315. st = &teWhitespace
  316. }
  317. str = alignTextHorizontal(str, horizontalAlign, width, st)
  318. }
  319. }
  320. if !inline {
  321. str = s.applyBorder(str)
  322. str = s.applyMargins(str, inline)
  323. }
  324. // Truncate according to MaxWidth
  325. if maxWidth > 0 {
  326. lines := strings.Split(str, "\n")
  327. for i := range lines {
  328. lines[i] = truncate.String(lines[i], uint(maxWidth))
  329. }
  330. str = strings.Join(lines, "\n")
  331. }
  332. // Truncate according to MaxHeight
  333. if maxHeight > 0 {
  334. lines := strings.Split(str, "\n")
  335. str = strings.Join(lines[:min(maxHeight, len(lines))], "\n")
  336. }
  337. return str
  338. }
  339. func (s Style) applyMargins(str string, inline bool) string {
  340. var (
  341. topMargin = s.getAsInt(marginTopKey)
  342. rightMargin = s.getAsInt(marginRightKey)
  343. bottomMargin = s.getAsInt(marginBottomKey)
  344. leftMargin = s.getAsInt(marginLeftKey)
  345. styler termenv.Style
  346. )
  347. bgc := s.getAsColor(marginBackgroundKey)
  348. if bgc != noColor {
  349. styler = styler.Background(bgc.color(s.r))
  350. }
  351. // Add left and right margin
  352. str = padLeft(str, leftMargin, &styler)
  353. str = padRight(str, rightMargin, &styler)
  354. // Top/bottom margin
  355. if !inline {
  356. _, width := getLines(str)
  357. spaces := strings.Repeat(" ", width)
  358. if topMargin > 0 {
  359. str = styler.Styled(strings.Repeat(spaces+"\n", topMargin)) + str
  360. }
  361. if bottomMargin > 0 {
  362. str += styler.Styled(strings.Repeat("\n"+spaces, bottomMargin))
  363. }
  364. }
  365. return str
  366. }
  367. // Apply left padding.
  368. func padLeft(str string, n int, style *termenv.Style) string {
  369. if n == 0 {
  370. return str
  371. }
  372. sp := strings.Repeat(" ", n)
  373. if style != nil {
  374. sp = style.Styled(sp)
  375. }
  376. b := strings.Builder{}
  377. l := strings.Split(str, "\n")
  378. for i := range l {
  379. b.WriteString(sp)
  380. b.WriteString(l[i])
  381. if i != len(l)-1 {
  382. b.WriteRune('\n')
  383. }
  384. }
  385. return b.String()
  386. }
  387. // Apply right padding.
  388. func padRight(str string, n int, style *termenv.Style) string {
  389. if n == 0 || str == "" {
  390. return str
  391. }
  392. sp := strings.Repeat(" ", n)
  393. if style != nil {
  394. sp = style.Styled(sp)
  395. }
  396. b := strings.Builder{}
  397. l := strings.Split(str, "\n")
  398. for i := range l {
  399. b.WriteString(l[i])
  400. b.WriteString(sp)
  401. if i != len(l)-1 {
  402. b.WriteRune('\n')
  403. }
  404. }
  405. return b.String()
  406. }
  407. func max(a, b int) int {
  408. if a > b {
  409. return a
  410. }
  411. return b
  412. }
  413. func min(a, b int) int {
  414. if a < b {
  415. return a
  416. }
  417. return b
  418. }