github.com/mithrandie/csvq@v1.18.1/lib/doc/writer.go (about) 1 package doc 2 3 import ( 4 "bufio" 5 "bytes" 6 "strings" 7 8 "github.com/mithrandie/csvq/lib/option" 9 "github.com/mithrandie/go-text/color" 10 ) 11 12 const DefaultPadding = 1 13 14 type Writer struct { 15 Flags *option.Flags 16 Palette *color.Palette 17 18 MaxWidth int 19 Padding int 20 Indent int 21 IndentWidth int 22 23 Title1 string 24 Title1Effect string 25 Title2 string 26 Title2Effect string 27 28 buf bytes.Buffer 29 30 subBlock int 31 lineWidth int 32 Column int 33 } 34 35 func NewWriter(screenWidth int, flags *option.Flags, palette *color.Palette) *Writer { 36 return &Writer{ 37 Flags: flags, 38 Palette: palette, 39 MaxWidth: screenWidth, 40 Indent: 0, 41 IndentWidth: 4, 42 Padding: DefaultPadding, 43 lineWidth: 0, 44 Column: 0, 45 subBlock: 0, 46 } 47 } 48 49 func (w *Writer) Clear() { 50 w.Title1 = "" 51 w.Title1Effect = "" 52 w.Title2 = "" 53 w.Title2Effect = "" 54 w.lineWidth = 0 55 w.Column = 0 56 w.subBlock = 0 57 w.buf.Reset() 58 } 59 60 func (w *Writer) WriteColorWithoutLineBreak(s string, effect string) { 61 w.write(s, effect, true) 62 } 63 64 func (w *Writer) WriteColor(s string, effect string) { 65 w.write(s, effect, false) 66 } 67 68 func (w *Writer) write(s string, effect string, withoutLineBreak bool) { 69 startOfLine := w.Column < 1 70 71 if startOfLine { 72 width := w.LeadingSpacesWidth() + w.subBlock 73 w.writeToBuf(strings.Repeat(" ", width)) 74 w.Column = width 75 } 76 77 if !withoutLineBreak && !startOfLine && !w.FitInLine(s) { 78 w.NewLine() 79 w.write(s, effect, withoutLineBreak) 80 } else { 81 if w.Palette == nil { 82 w.writeToBuf(s) 83 } else { 84 w.writeToBuf(w.Palette.Render(effect, s)) 85 } 86 w.Column = w.Column + option.TextWidth(s, w.Flags) 87 } 88 } 89 90 func (w *Writer) writeToBuf(s string) { 91 w.buf.WriteString(s) 92 } 93 94 func (w *Writer) LeadingSpacesWidth() int { 95 return w.Padding + (w.Indent * w.IndentWidth) 96 } 97 98 func (w *Writer) FitInLine(s string) bool { 99 if w.MaxWidth-(w.Padding*2)-1 < w.Column+option.TextWidth(s, w.Flags) { 100 return false 101 } 102 return true 103 } 104 105 func (w *Writer) WriteWithoutLineBreak(s string) { 106 w.WriteColorWithoutLineBreak(s, option.NoEffect) 107 } 108 109 func (w *Writer) Write(s string) { 110 w.WriteColor(s, option.NoEffect) 111 } 112 113 func (w *Writer) WriteWithAutoLineBreak(s string) { 114 w.writeWithAutoLineBreak(s, false, true) 115 } 116 117 func (w *Writer) WriteWithAutoLineBreakWithContinueMark(s string) { 118 w.writeWithAutoLineBreak(s, true, false) 119 } 120 121 func (w *Writer) writeWithAutoLineBreak(s string, useContinueMark bool, useBlock bool) { 122 continueMark := "" 123 if useContinueMark { 124 continueMark = "\\" 125 } 126 127 scanner := bufio.NewScanner(strings.NewReader(s)) 128 firstLine := true 129 blockQuote := false 130 preformatted := false 131 for scanner.Scan() { 132 if blockQuote { 133 w.EndBlock() 134 blockQuote = false 135 } 136 137 line := scanner.Text() 138 if useBlock && option.TrimSpace(line) == "```" { 139 preformatted = !preformatted 140 continue 141 } else { 142 if firstLine { 143 firstLine = false 144 } else { 145 w.NewLine() 146 } 147 } 148 149 if preformatted { 150 w.Write(line) 151 continue 152 } 153 154 wscanner := bufio.NewScanner(strings.NewReader(line)) 155 wscanner.Split(bufio.ScanWords) 156 lineHead := true 157 158 for wscanner.Scan() { 159 word := wscanner.Text() 160 if lineHead { 161 if useBlock && blockQuote == false && word == ">" { 162 blockQuote = true 163 w.BeginBlock() 164 continue 165 } 166 167 lineHead = false 168 } else { 169 if !w.FitInLine(" " + word + continueMark) { 170 w.Write(continueMark) 171 w.NewLine() 172 } else { 173 word = " " + word 174 } 175 } 176 177 w.Write(word) 178 } 179 } 180 181 if blockQuote { 182 w.EndBlock() 183 } 184 } 185 186 func (w *Writer) WriteSpaces(l int) { 187 w.Write(strings.Repeat(" ", l)) 188 } 189 190 func (w *Writer) NewLine() { 191 w.buf.WriteRune('\n') 192 if w.lineWidth < w.Column { 193 w.lineWidth = w.Column 194 } 195 w.Column = 0 196 } 197 198 func (w *Writer) BeginBlock() { 199 w.Indent++ 200 } 201 202 func (w *Writer) EndBlock() { 203 w.Indent-- 204 } 205 206 func (w *Writer) BeginSubBlock() { 207 w.subBlock = w.Column - w.LeadingSpacesWidth() 208 } 209 210 func (w *Writer) EndSubBlock() { 211 w.subBlock = 0 212 } 213 214 func (w *Writer) ClearBlock() { 215 w.Indent = 0 216 } 217 218 func (w *Writer) String() string { 219 var header bytes.Buffer 220 if 0 < len(w.Title1) || 0 < len(w.Title2) { 221 tw := option.TextWidth(w.Title1, w.Flags) + option.TextWidth(w.Title2, w.Flags) 222 if 0 < len(w.Title1) && 0 < len(w.Title2) { 223 tw++ 224 } 225 226 hlLen := tw + 2 227 if hlLen < w.lineWidth+1 { 228 hlLen = w.lineWidth + 1 229 } 230 if hlLen < w.Column+1 { 231 hlLen = w.Column + 1 232 } 233 if w.MaxWidth < hlLen { 234 hlLen = w.MaxWidth 235 } 236 237 if tw < hlLen { 238 header.Write(bytes.Repeat([]byte(" "), (hlLen-tw)/2)) 239 } 240 if 0 < len(w.Title1) { 241 if w.Palette == nil { 242 header.WriteString(w.Title1) 243 } else { 244 header.WriteString(w.Palette.Render(w.Title1Effect, w.Title1)) 245 } 246 } 247 if 0 < len(w.Title2) { 248 header.WriteRune(' ') 249 if w.Palette == nil { 250 header.WriteString(w.Title2) 251 } else { 252 header.WriteString(w.Palette.Render(w.Title2Effect, w.Title2)) 253 } 254 } 255 header.WriteRune('\n') 256 header.Write(bytes.Repeat([]byte("-"), hlLen)) 257 header.WriteRune('\n') 258 } 259 260 return header.String() + w.buf.String() 261 }