okami.mom/glamour

git clone https://git.nkp.pet/okami.mom/glamour

Kieran Klukaschore: fix pre-existing lint failuresc293649

main
5.5 KiB231 linesraw
1package ansi
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"io"
8	"strings"
9	"unicode/utf8"
10
11	"charm.land/lipgloss/v2"
12	"github.com/charmbracelet/x/ansi"
13)
14
15// MarginWriter is a Writer that applies indentation and padding around
16// whatever you write to it.
17type MarginWriter struct {
18	w  io.Writer
19	iw *IndentWriter
20}
21
22// NewMarginWriter returns a new MarginWriter.
23func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock) *MarginWriter {
24	bs := ctx.blockStack
25
26	var indentation uint
27	var margin uint
28	if rules.Indent != nil {
29		indentation = *rules.Indent
30	}
31	if rules.Margin != nil {
32		margin = *rules.Margin
33	}
34
35	pw := NewPaddingWriter(w, int(bs.Width(ctx)), func(_ io.Writer) {
36		_, _ = renderText(w, rules.StylePrimitive, " ")
37	})
38
39	ic := " "
40	if rules.IndentToken != nil {
41		ic = *rules.IndentToken
42	}
43	iw := NewIndentWriter(pw, int(indentation+margin), func(_ io.Writer) {
44		_, _ = renderText(w, bs.Parent().Style.StylePrimitive, ic)
45	})
46
47	return &MarginWriter{
48		w:  lipgloss.NewWrapWriter(w),
49		iw: iw,
50	}
51}
52
53// Write writes to the margin writer and implements [io.Writer].
54func (w *MarginWriter) Write(b []byte) (int, error) {
55	n, err := w.iw.Write(b)
56	if err != nil {
57		return 0, fmt.Errorf("glamour: error writing bytes: %w", err)
58	}
59	return n, nil
60}
61
62// Close closes the [MarginWriter].
63func (w *MarginWriter) Close() error {
64	var werr error
65	if c, ok := w.w.(io.WriteCloser); ok {
66		werr = c.Close()
67	}
68
69	return errors.Join(werr, w.iw.Close())
70}
71
72// PaddingFunc is a function that applies padding around whatever you write to it.
73type PaddingFunc = func(w io.Writer)
74
75// PaddingWriter is a writer that applies padding around whatever you write to
76// it.
77type PaddingWriter struct {
78	Padding int
79	PadFunc PaddingFunc
80	w       *lipgloss.WrapWriter
81	cache   bytes.Buffer
82}
83
84// NewPaddingWriter returns a new PaddingWriter.
85func NewPaddingWriter(w io.Writer, padding int, padFunc PaddingFunc) *PaddingWriter {
86	return &PaddingWriter{
87		Padding: padding,
88		PadFunc: padFunc,
89		w:       lipgloss.NewWrapWriter(w),
90	}
91}
92
93// Write writes to the padding writer.
94func (w *PaddingWriter) Write(p []byte) (int, error) {
95	// Use UTF-8 aware iteration to properly handle multi-byte characters (e.g., CJK)
96	for i := 0; i < len(p); {
97		r, size := utf8.DecodeRune(p[i:])
98		if r == '\n' { //nolint:nestif
99			line := w.cache.String()
100			linew := ansi.StringWidth(line)
101			if w.Padding > 0 && linew < w.Padding {
102				if w.PadFunc != nil {
103					for n := 0; n < w.Padding-linew; n++ {
104						w.PadFunc(w.w)
105					}
106				} else {
107					_, err := io.WriteString(w.w, strings.Repeat(" ", w.Padding-linew))
108					if err != nil {
109						return 0, fmt.Errorf("glamour: error writing padding: %w", err)
110					}
111				}
112			}
113			w.cache.Reset()
114		} else {
115			// Write complete UTF-8 character bytes to cache
116			w.cache.Write(p[i : i+size])
117		}
118
119		// Write complete UTF-8 character bytes to output
120		_, err := w.w.Write(p[i : i+size])
121		if err != nil {
122			return 0, fmt.Errorf("glamour: error writing bytes: %w", err)
123		}
124		i += size
125	}
126
127	return len(p), nil
128}
129
130// Close closes the [PaddingWriter].
131func (w *PaddingWriter) Close() error {
132	return w.w.Close() //nolint:wrapcheck
133}
134
135// IndentFunc is a function that applies indentation around whatever you write to
136// it.
137type IndentFunc = func(w io.Writer)
138
139// IndentWriter is a writer that applies indentation around whatever you write to
140// it.
141type IndentWriter struct {
142	Indent     int
143	IndentFunc PaddingFunc
144	w          io.Writer
145	pw         *lipgloss.WrapWriter
146	skipIndent bool
147}
148
149// NewIndentWriter returns a new IndentWriter.
150func NewIndentWriter(w io.Writer, indent int, indentFunc IndentFunc) *IndentWriter {
151	return &IndentWriter{
152		Indent:     indent,
153		IndentFunc: indentFunc,
154		pw:         lipgloss.NewWrapWriter(w),
155		w:          w,
156	}
157}
158
159func (w *IndentWriter) resetPen() {
160	style := w.pw.Style()
161	link := w.pw.Link()
162	if !style.IsZero() {
163		_, _ = io.WriteString(w.w, ansi.ResetStyle)
164	}
165	if !link.IsZero() {
166		_, _ = io.WriteString(w.w, ansi.ResetHyperlink())
167	}
168}
169
170func (w *IndentWriter) restorePen() {
171	style := w.pw.Style()
172	link := w.pw.Link()
173	if !style.IsZero() {
174		_, _ = io.WriteString(w.w, style.String())
175	}
176	if !link.IsZero() {
177		_, _ = io.WriteString(w.w, ansi.SetHyperlink(link.URL, link.Params))
178	}
179}
180
181// Write writes to the indentation writer.
182func (w *IndentWriter) Write(p []byte) (int, error) {
183	// Use UTF-8 aware iteration to properly handle multi-byte characters (e.g., CJK)
184	for i := 0; i < len(p); {
185		r, size := utf8.DecodeRune(p[i:])
186		if !w.skipIndent {
187			w.resetPen()
188			if w.IndentFunc != nil {
189				for j := 0; j < w.Indent; j++ {
190					w.IndentFunc(w.pw)
191				}
192			} else {
193				_, err := io.WriteString(w.pw, strings.Repeat(" ", w.Indent))
194				if err != nil {
195					return 0, fmt.Errorf("glamour: error writing indentation: %w", err)
196				}
197			}
198
199			w.skipIndent = true
200			w.restorePen()
201		}
202
203		if r == '\n' {
204			w.skipIndent = false
205		}
206
207		// Write complete UTF-8 character bytes to output
208		_, err := w.pw.Write(p[i : i+size])
209		if err != nil {
210			return 0, fmt.Errorf("glamour: error writing bytes: %w", err)
211		}
212		i += size
213	}
214
215	return len(p), nil
216}
217
218// Close closes the [IndentWriter].
219func (w *IndentWriter) Close() error {
220	// Close the wrap writer (w.pw) before the downstream writer (w.w). w.pw
221	// wraps w.w, so its Close flushes a trailing style/link reset back through
222	// w.w. Closing w.w first would return its parser to the pool and nil it
223	// out, turning that flush into a write on a closed writer.
224	werr := w.pw.Close()
225
226	if c, ok := w.w.(io.WriteCloser); ok {
227		werr = errors.Join(werr, c.Close())
228	}
229
230	return werr
231}