wrap.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. package ansi
  2. import (
  3. "bytes"
  4. "unicode"
  5. "unicode/utf8"
  6. "github.com/charmbracelet/x/ansi/parser"
  7. "github.com/rivo/uniseg"
  8. )
  9. // nbsp is a non-breaking space
  10. const nbsp = 0xA0
  11. // Hardwrap wraps a string or a block of text to a given line length, breaking
  12. // word boundaries. This will preserve ANSI escape codes and will account for
  13. // wide-characters in the string.
  14. // When preserveSpace is true, spaces at the beginning of a line will be
  15. // preserved.
  16. func Hardwrap(s string, limit int, preserveSpace bool) string {
  17. if limit < 1 {
  18. return s
  19. }
  20. var (
  21. cluster []byte
  22. buf bytes.Buffer
  23. curWidth int
  24. forceNewline bool
  25. gstate = -1
  26. pstate = parser.GroundState // initial state
  27. b = []byte(s)
  28. )
  29. addNewline := func() {
  30. buf.WriteByte('\n')
  31. curWidth = 0
  32. }
  33. i := 0
  34. for i < len(b) {
  35. state, action := parser.Table.Transition(pstate, b[i])
  36. switch action {
  37. case parser.PrintAction:
  38. if utf8ByteLen(b[i]) > 1 {
  39. var width int
  40. cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
  41. i += len(cluster)
  42. if curWidth+width > limit {
  43. addNewline()
  44. }
  45. if !preserveSpace && curWidth == 0 && len(cluster) <= 4 {
  46. // Skip spaces at the beginning of a line
  47. if r, _ := utf8.DecodeRune(cluster); r != utf8.RuneError && unicode.IsSpace(r) {
  48. pstate = parser.GroundState
  49. continue
  50. }
  51. }
  52. buf.Write(cluster)
  53. curWidth += width
  54. gstate = -1 // reset grapheme state otherwise, width calculation might be off
  55. pstate = parser.GroundState
  56. continue
  57. }
  58. fallthrough
  59. case parser.ExecuteAction:
  60. if b[i] == '\n' {
  61. addNewline()
  62. forceNewline = false
  63. break
  64. }
  65. if curWidth+1 > limit {
  66. addNewline()
  67. forceNewline = true
  68. }
  69. // Skip spaces at the beginning of a line
  70. if curWidth == 0 {
  71. if !preserveSpace && forceNewline && unicode.IsSpace(rune(b[i])) {
  72. break
  73. }
  74. forceNewline = false
  75. }
  76. buf.WriteByte(b[i])
  77. curWidth++
  78. default:
  79. buf.WriteByte(b[i])
  80. }
  81. // We manage the UTF8 state separately manually above.
  82. if pstate != parser.Utf8State {
  83. pstate = state
  84. }
  85. i++
  86. }
  87. return buf.String()
  88. }
  89. // Wordwrap wraps a string or a block of text to a given line length, not
  90. // breaking word boundaries. This will preserve ANSI escape codes and will
  91. // account for wide-characters in the string.
  92. // The breakpoints string is a list of characters that are considered
  93. // breakpoints for word wrapping. A hyphen (-) is always considered a
  94. // breakpoint.
  95. //
  96. // Note: breakpoints must be a string of 1-cell wide rune characters.
  97. func Wordwrap(s string, limit int, breakpoints string) string {
  98. if limit < 1 {
  99. return s
  100. }
  101. var (
  102. cluster []byte
  103. buf bytes.Buffer
  104. word bytes.Buffer
  105. space bytes.Buffer
  106. curWidth int
  107. wordLen int
  108. gstate = -1
  109. pstate = parser.GroundState // initial state
  110. b = []byte(s)
  111. )
  112. addSpace := func() {
  113. curWidth += space.Len()
  114. buf.Write(space.Bytes())
  115. space.Reset()
  116. }
  117. addWord := func() {
  118. if word.Len() == 0 {
  119. return
  120. }
  121. addSpace()
  122. curWidth += wordLen
  123. buf.Write(word.Bytes())
  124. word.Reset()
  125. wordLen = 0
  126. }
  127. addNewline := func() {
  128. buf.WriteByte('\n')
  129. curWidth = 0
  130. space.Reset()
  131. }
  132. i := 0
  133. for i < len(b) {
  134. state, action := parser.Table.Transition(pstate, b[i])
  135. switch action {
  136. case parser.PrintAction:
  137. if utf8ByteLen(b[i]) > 1 {
  138. var width int
  139. cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
  140. i += len(cluster)
  141. r, _ := utf8.DecodeRune(cluster)
  142. if r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp {
  143. addWord()
  144. space.WriteRune(r)
  145. } else if bytes.ContainsAny(cluster, breakpoints) {
  146. addSpace()
  147. addWord()
  148. buf.Write(cluster)
  149. curWidth++
  150. } else {
  151. word.Write(cluster)
  152. wordLen += width
  153. if curWidth+space.Len()+wordLen > limit &&
  154. wordLen < limit {
  155. addNewline()
  156. }
  157. }
  158. pstate = parser.GroundState
  159. continue
  160. }
  161. fallthrough
  162. case parser.ExecuteAction:
  163. r := rune(b[i])
  164. switch {
  165. case r == '\n':
  166. if wordLen == 0 {
  167. if curWidth+space.Len() > limit {
  168. curWidth = 0
  169. } else {
  170. buf.Write(space.Bytes())
  171. }
  172. space.Reset()
  173. }
  174. addWord()
  175. addNewline()
  176. case unicode.IsSpace(r):
  177. addWord()
  178. space.WriteByte(b[i])
  179. case r == '-':
  180. fallthrough
  181. case runeContainsAny(r, breakpoints):
  182. addSpace()
  183. addWord()
  184. buf.WriteByte(b[i])
  185. curWidth++
  186. default:
  187. word.WriteByte(b[i])
  188. wordLen++
  189. if curWidth+space.Len()+wordLen > limit &&
  190. wordLen < limit {
  191. addNewline()
  192. }
  193. }
  194. default:
  195. word.WriteByte(b[i])
  196. }
  197. // We manage the UTF8 state separately manually above.
  198. if pstate != parser.Utf8State {
  199. pstate = state
  200. }
  201. i++
  202. }
  203. addWord()
  204. return buf.String()
  205. }
  206. // Wrap wraps a string or a block of text to a given line length, breaking word
  207. // boundaries if necessary. This will preserve ANSI escape codes and will
  208. // account for wide-characters in the string. The breakpoints string is a list
  209. // of characters that are considered breakpoints for word wrapping. A hyphen
  210. // (-) is always considered a breakpoint.
  211. //
  212. // Note: breakpoints must be a string of 1-cell wide rune characters.
  213. func Wrap(s string, limit int, breakpoints string) string {
  214. if limit < 1 {
  215. return s
  216. }
  217. var (
  218. cluster []byte
  219. buf bytes.Buffer
  220. word bytes.Buffer
  221. space bytes.Buffer
  222. curWidth int // written width of the line
  223. wordLen int // word buffer len without ANSI escape codes
  224. gstate = -1
  225. pstate = parser.GroundState // initial state
  226. b = []byte(s)
  227. )
  228. addSpace := func() {
  229. curWidth += space.Len()
  230. buf.Write(space.Bytes())
  231. space.Reset()
  232. }
  233. addWord := func() {
  234. if word.Len() == 0 {
  235. return
  236. }
  237. addSpace()
  238. curWidth += wordLen
  239. buf.Write(word.Bytes())
  240. word.Reset()
  241. wordLen = 0
  242. }
  243. addNewline := func() {
  244. buf.WriteByte('\n')
  245. curWidth = 0
  246. space.Reset()
  247. }
  248. i := 0
  249. for i < len(b) {
  250. state, action := parser.Table.Transition(pstate, b[i])
  251. switch action {
  252. case parser.PrintAction:
  253. if utf8ByteLen(b[i]) > 1 {
  254. var width int
  255. cluster, _, width, gstate = uniseg.FirstGraphemeCluster(b[i:], gstate)
  256. i += len(cluster)
  257. r, _ := utf8.DecodeRune(cluster)
  258. switch {
  259. case r != utf8.RuneError && unicode.IsSpace(r) && r != nbsp: // nbsp is a non-breaking space
  260. addWord()
  261. space.WriteRune(r)
  262. case bytes.ContainsAny(cluster, breakpoints):
  263. addSpace()
  264. if curWidth+wordLen+width > limit {
  265. word.Write(cluster)
  266. wordLen += width
  267. } else {
  268. addWord()
  269. buf.Write(cluster)
  270. curWidth += width
  271. }
  272. default:
  273. if wordLen+width > limit {
  274. // Hardwrap the word if it's too long
  275. addWord()
  276. }
  277. word.Write(cluster)
  278. wordLen += width
  279. if curWidth+wordLen+space.Len() > limit {
  280. addNewline()
  281. }
  282. }
  283. pstate = parser.GroundState
  284. continue
  285. }
  286. fallthrough
  287. case parser.ExecuteAction:
  288. switch r := rune(b[i]); {
  289. case r == '\n':
  290. if wordLen == 0 {
  291. if curWidth+space.Len() > limit {
  292. curWidth = 0
  293. } else {
  294. // preserve whitespaces
  295. buf.Write(space.Bytes())
  296. }
  297. space.Reset()
  298. }
  299. addWord()
  300. addNewline()
  301. case unicode.IsSpace(r):
  302. addWord()
  303. space.WriteRune(r)
  304. case r == '-':
  305. fallthrough
  306. case runeContainsAny(r, breakpoints):
  307. addSpace()
  308. if curWidth+wordLen >= limit {
  309. // We can't fit the breakpoint in the current line, treat
  310. // it as part of the word.
  311. word.WriteRune(r)
  312. wordLen++
  313. } else {
  314. addWord()
  315. buf.WriteRune(r)
  316. curWidth++
  317. }
  318. default:
  319. word.WriteRune(r)
  320. wordLen++
  321. if wordLen == limit {
  322. // Hardwrap the word if it's too long
  323. addWord()
  324. }
  325. if curWidth+wordLen+space.Len() > limit {
  326. addNewline()
  327. }
  328. }
  329. default:
  330. word.WriteByte(b[i])
  331. }
  332. // We manage the UTF8 state separately manually above.
  333. if pstate != parser.Utf8State {
  334. pstate = state
  335. }
  336. i++
  337. }
  338. if word.Len() != 0 {
  339. // Preserve ANSI wrapped spaces at the end of string
  340. if curWidth+space.Len() > limit {
  341. buf.WriteByte('\n')
  342. }
  343. addSpace()
  344. }
  345. buf.Write(word.Bytes())
  346. return buf.String()
  347. }
  348. func runeContainsAny(r rune, s string) bool {
  349. for _, c := range s {
  350. if c == r {
  351. return true
  352. }
  353. }
  354. return false
  355. }