diff --git a/internal/render/command.go b/internal/render/command.go index 4b36dd7..f6e5bbd 100644 --- a/internal/render/command.go +++ b/internal/render/command.go @@ -13,7 +13,7 @@ import ( // so that the segment's style can be applied // during line-by-line rendering. // -// spaceAfter reports whether a space originally +// followedBySpace reports whether a space originally // followed this word in the source, // so commandText can reconstruct // the display string faithfully @@ -24,13 +24,14 @@ type mappedWord struct { text string } -// renderCommand writes a styled, wrapped command to w. +// renderCommand writes a styled, +// wrapped command to the configured [io.Writer]. // // It decomposes segments into word-level mappings, // wraps the combined text to fit r.output.LineLength, // and renders each wrapped line with per-word // segment styling via renderCommandLine. -func (r *Renderer) renderCommand(w io.Writer, segments []Segment) error { +func (r *Renderer) renderCommand(segments []Segment) error { mappedWords := mapWords(segments, r.output.OptionStyle) if len(mappedWords) == 0 { return nil @@ -53,7 +54,6 @@ func (r *Renderer) renderCommand(w io.Writer, segments []Segment) error { } if err := r.renderCommandLine( - w, words, mappedWords, segments, @@ -72,18 +72,17 @@ func (r *Renderer) renderCommand(w io.Writer, segments []Segment) error { // wordOffset tracks the current position in mappedWords // across multi-line rendering. // -// Consecutive mappedWords with spaceAfter=false form a single +// Consecutive mappedWords with followedBySpace = false form a single // whitespace-delimited field in the wrapped display text; // each sub-word is rendered with its own segment's style. func (r *Renderer) renderCommandLine( - w io.Writer, words []string, mappedWords []mappedWord, segments []Segment, indent string, wordOffset *int, ) error { - _, err := io.WriteString(w, indent) + _, err := io.WriteString(r.w, indent) if err != nil { return err } @@ -106,7 +105,7 @@ func (r *Renderer) renderCommandLine( mw := mappedWords[k] seg := segments[mw.segmentIndex] if _, err := io.WriteString( - w, + r.w, r.applyStyle( r.styleForSegment(&seg), mw.text, @@ -117,14 +116,14 @@ func (r *Renderer) renderCommandLine( } if j < len(words)-1 { - _, err := io.WriteString(w, " ") + _, err := io.WriteString(r.w, " ") if err != nil { return err } } } - _, err = io.WriteString(w, "\n") + _, err = io.WriteString(r.w, "\n") return err } @@ -165,7 +164,7 @@ func mapWords(segments []Segment, optionStyle config.OptionStyle) []mappedWord { // commandText joins the text fields of mapped words back // into a single space-separated string, suitable for text wrapping. // A space is inserted only where the original source had whitespace -// (recorded in spaceAfter), so punctuation spacing is preserved +// (recorded in followedBySpace), so punctuation spacing is preserved // faithfully without a hard-coded punctuation list. func commandText(words []mappedWord) string { var b strings.Builder diff --git a/internal/render/command_test.go b/internal/render/command_test.go index 63d3858..c6d251c 100644 --- a/internal/render/command_test.go +++ b/internal/render/command_test.go @@ -84,7 +84,9 @@ func TestRenderCommand(t *testing.T) { t.Run(tt.name, func(t *testing.T) { segments := ParseCommand(tt.raw) var buf strings.Builder - err := tt.renderer.renderCommand(&buf, segments) + w := io.Writer(&buf) + tt.renderer.w = w + err := tt.renderer.renderCommand(segments) assert.NoError(t, err) assert.Equal(t, tt.want, buf.String()) }) @@ -94,7 +96,10 @@ func TestRenderCommand(t *testing.T) { func TestRenderCommand_Wrapping(t *testing.T) { t.Parallel() + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: false, output: config.OutputConfig{ OptionStyle: config.OptionStyleLong, @@ -105,8 +110,7 @@ func TestRenderCommand_Wrapping(t *testing.T) { }, } - var buf strings.Builder - err := r.renderCommand(&buf, ParseCommand("some very long command")) + err := r.renderCommand(ParseCommand("some very long command")) assert.NoError(t, err) assert.Equal(t, " some very long\n command\n", buf.String()) } @@ -114,7 +118,10 @@ func TestRenderCommand_Wrapping(t *testing.T) { func TestRenderCommand_ShortOption(t *testing.T) { t.Parallel() + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: false, output: config.OutputConfig{ OptionStyle: config.OptionStyleShort, @@ -125,8 +132,7 @@ func TestRenderCommand_ShortOption(t *testing.T) { }, } - var buf strings.Builder - err := r.renderCommand(&buf, ParseCommand("cmd {{[-s|--long]}}")) + err := r.renderCommand(ParseCommand("cmd {{[-s|--long]}}")) assert.NoError(t, err) assert.Equal(t, " cmd -s\n", buf.String()) } @@ -134,7 +140,10 @@ func TestRenderCommand_ShortOption(t *testing.T) { func TestRenderCommand_CombinedOption(t *testing.T) { t.Parallel() + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: false, output: config.OutputConfig{ OptionStyle: config.OptionStyleCombined, @@ -144,8 +153,8 @@ func TestRenderCommand_CombinedOption(t *testing.T) { Example: 4, }, } - var buf strings.Builder - err := r.renderCommand(&buf, ParseCommand("cmd {{[-s|--long]}}")) + + err := r.renderCommand(ParseCommand("cmd {{[-s|--long]}}")) assert.NoError(t, err) assert.Equal(t, " cmd [-s|--long]\n", buf.String()) } @@ -153,7 +162,10 @@ func TestRenderCommand_CombinedOption(t *testing.T) { func TestRenderCommand_Colorized(t *testing.T) { t.Parallel() + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: true, style: config.DefaultStyleConfig(), output: config.OutputConfig{ @@ -165,8 +177,7 @@ func TestRenderCommand_Colorized(t *testing.T) { }, } - var buf strings.Builder - err := r.renderCommand(&buf, ParseCommand("echo hello")) + err := r.renderCommand(ParseCommand("echo hello")) assert.NoError(t, err) output := buf.String() assert.Contains(t, output, "\x1b[36m") @@ -179,6 +190,9 @@ func TestRenderCommand_WriteError(t *testing.T) { t.Parallel() r := &Renderer{ + w: &errorWriter{ + err: errors.New("write error"), + }, useColor: false, output: config.OutputConfig{ OptionStyle: config.OptionStyleLong, @@ -189,7 +203,7 @@ func TestRenderCommand_WriteError(t *testing.T) { }, } - err := r.renderCommand(&errorWriter{err: errors.New("write error")}, ParseCommand("echo hi")) + err := r.renderCommand(ParseCommand("echo hi")) assert.ErrorContains(t, err, "write error") } @@ -273,12 +287,12 @@ func TestRenderCommandLine(t *testing.T) { var buf strings.Builder w := io.Writer(&buf) + r.w = w if tt.writer != nil { - w = tt.writer + r.w = tt.writer } err := r.renderCommandLine( - w, tt.words, tt.mappedWords, tt.segments, diff --git a/internal/render/description.go b/internal/render/description.go index f8a3585..d2d03b9 100644 --- a/internal/render/description.go +++ b/internal/render/description.go @@ -8,9 +8,8 @@ import ( // renderDescriptionLine writes one description line, // styled with r.style.Description, indented, // and followed by a newline. -func (r *Renderer) renderDescriptionLine(w io.Writer, text, indent string) error { +func (r *Renderer) renderDescriptionLine(text, indent string) error { if err := r.renderStyledInline( - w, text, indent, r.style.Description, @@ -19,7 +18,7 @@ func (r *Renderer) renderDescriptionLine(w io.Writer, text, indent string) error return err } - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") return err } @@ -27,7 +26,7 @@ func (r *Renderer) renderDescriptionLine(w io.Writer, text, indent string) error // followed by the "More information" URL (if set), // each indented by r.indent.Description. // Writes a trailing blank line after descriptions. -func (r *Renderer) renderDescriptions(w io.Writer, descs []string, url string) error { +func (r *Renderer) renderDescriptions(descs []string, url string) error { if len(descs) == 0 && url == "" { return nil } @@ -35,19 +34,19 @@ func (r *Renderer) renderDescriptions(w io.Writer, descs []string, url string) e indent := strings.Repeat(" ", r.indent.Description) for _, d := range descs { - if err := r.renderDescriptionLine(w, d, indent); err != nil { + if err := r.renderDescriptionLine(d, indent); err != nil { return err } } if url != "" { - if err := r.renderDescriptionURL(w, url, indent); err != nil { + if err := r.renderDescriptionURL(url, indent); err != nil { return err } } if !r.output.Compact { - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") return err } @@ -59,7 +58,7 @@ func (r *Renderer) renderDescriptions(w io.Writer, descs []string, url string) e // The "More information: " prefix and trailing "." use r.style.Description; // the URL itself uses r.style.URL. // The line is indented and wrapped via wrapText. -func (r *Renderer) renderDescriptionURL(w io.Writer, url, indent string) error { +func (r *Renderer) renderDescriptionURL(url, indent string) error { var text strings.Builder text.WriteString( r.applyStyle( @@ -83,7 +82,7 @@ func (r *Renderer) renderDescriptionURL(w io.Writer, url, indent string) error { wrappedText := r.wrapText(text.String(), indent) _, err := io.WriteString( - w, + r.w, wrappedText+"\n", ) return err @@ -92,9 +91,8 @@ func (r *Renderer) renderDescriptionURL(w io.Writer, url, indent string) error { // renderBulletLine writes one bullet item line, // styled with r.style.Bullet and indented. // No trailing newline is added. -func (r *Renderer) renderBulletLine(w io.Writer, text, indent string) error { +func (r *Renderer) renderBulletLine(text, indent string) error { return r.renderStyledInline( - w, text, indent, r.style.Bullet, diff --git a/internal/render/description_test.go b/internal/render/description_test.go index 278fe75..4cac42a 100644 --- a/internal/render/description_test.go +++ b/internal/render/description_test.go @@ -39,15 +39,16 @@ func TestRenderDescriptionLine(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := &Renderer{} - var buf strings.Builder w := io.Writer(&buf) + r := &Renderer{ + w: w, + } if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderDescriptionLine(w, tt.text, tt.indent) + err := r.renderDescriptionLine(tt.text, tt.indent) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) @@ -121,18 +122,19 @@ func TestRenderDescriptions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, indent: config.IndentConfig{Description: 2}, output: config.OutputConfig{Compact: tt.compact}, } - var buf strings.Builder - w := io.Writer(&buf) if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderDescriptions(w, tt.descs, tt.url) + err := r.renderDescriptions(tt.descs, tt.url) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) @@ -188,7 +190,9 @@ func TestRenderDescriptionURL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: tt.useColor, style: config.StyleConfig{ Description: tt.descStyle, @@ -197,12 +201,11 @@ func TestRenderDescriptionURL(t *testing.T) { output: config.OutputConfig{LineLength: tt.lineLength}, } - w := io.Writer(&buf) if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderDescriptionURL(w, tt.url, tt.indent) + err := r.renderDescriptionURL(tt.url, tt.indent) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) @@ -251,15 +254,17 @@ func TestRenderBulletLine(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := &Renderer{} - var buf strings.Builder w := io.Writer(&buf) + r := &Renderer{ + w: w, + } + if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderBulletLine(w, tt.text, tt.indent) + err := r.renderBulletLine(tt.text, tt.indent) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) diff --git a/internal/render/edit.go b/internal/render/edit.go index 1886ca5..d32061a 100644 --- a/internal/render/edit.go +++ b/internal/render/edit.go @@ -8,17 +8,17 @@ import ( "github.com/TheRootDaemon/tlgc/pathutil" ) -// renderEditLink writes the edit URL to w, with a trailing newline -// unless output is in compact mode. -func (r *Renderer) renderEditLink(w io.Writer, url string) error { +// renderEditLink writes the edit URL to the configured [io.Writer], +// with a trailing newline unless output is in compact mode. +func (r *Renderer) renderEditLink(url string) error { logger.Info("edit this page on GitHub") - _, err := io.WriteString(w, url) + _, err := io.WriteString(r.w, url) if err != nil { return err } if !r.output.Compact { - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") return err } @@ -37,7 +37,7 @@ func (r *Renderer) renderPageEditLink(p *Page) error { return nil } - return r.renderEditLink(r.w, url) + return r.renderEditLink(url) } // BuildViewURL returns the GitHub blob URL for a tldr page. diff --git a/internal/render/edit_test.go b/internal/render/edit_test.go index b513823..d526084 100644 --- a/internal/render/edit_test.go +++ b/internal/render/edit_test.go @@ -45,17 +45,18 @@ func TestRenderEditLink(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, output: config.OutputConfig{Compact: tt.compact}, } - var buf strings.Builder - w := io.Writer(&buf) if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderEditLink(w, tt.url) + err := r.renderEditLink(tt.url) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) diff --git a/internal/render/example.go b/internal/render/example.go index 4116905..11036ba 100644 --- a/internal/render/example.go +++ b/internal/render/example.go @@ -9,33 +9,33 @@ import ( // a bullet line for the description (prefixed with ExamplePrefix when ShowHyphens is set) // followed by the styled command text on the next line. // In compact mode the blank line between bullet and command is omitted. -func (r *Renderer) renderExample(w io.Writer, ex Example) error { +func (r *Renderer) renderExample(example Example) error { indent := strings.Repeat(" ", r.indent.Bullet) - desc := ex.Description + desc := example.Description if r.output.ShowHyphens { desc = r.output.ExamplePrefix + desc } - if err := r.renderBulletLine(w, desc, indent); err != nil { + if err := r.renderBulletLine(desc, indent); err != nil { return err } if !r.output.Compact { - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") if err != nil { return err } } - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") if err != nil { return err } - if ex.Command != "" { - segments := ParseCommand(ex.Command) - if err := r.renderCommand(w, segments); err != nil { + if example.Command != "" { + segments := ParseCommand(example.Command) + if err := r.renderCommand(segments); err != nil { return err } } @@ -43,21 +43,21 @@ func (r *Renderer) renderExample(w io.Writer, ex Example) error { return nil } -// renderExamples renders a sequence of examples to w. +// renderExamples renders a sequence of examples to the configured [io.Writer]. // // Each example is rendered using renderExample. // In non-compact mode, a blank line is inserted // between consecutive examples -func (r *Renderer) renderExamples(w io.Writer, examples []Example) error { - for i, ex := range examples { +func (r *Renderer) renderExamples(examples []Example) error { + for i, example := range examples { if i > 0 && !r.output.Compact { - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") if err != nil { return err } } - if err := r.renderExample(r.w, ex); err != nil { + if err := r.renderExample(example); err != nil { return err } } diff --git a/internal/render/example_test.go b/internal/render/example_test.go index 6993775..9424107 100644 --- a/internal/render/example_test.go +++ b/internal/render/example_test.go @@ -60,19 +60,20 @@ func TestRenderExample(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: false, output: tt.output, indent: tt.indent, } - var buf strings.Builder - w := io.Writer(&buf) if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderExample(w, tt.ex) + err := r.renderExample(tt.ex) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) @@ -139,19 +140,18 @@ func TestRenderExamples(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ - w: &buf, + w: w, output: config.OutputConfig{Compact: tt.compact}, indent: tt.indent, } - w := io.Writer(&buf) if tt.writer != nil { r.w = tt.writer - w = tt.writer } - err := r.renderExamples(w, tt.examples) + err := r.renderExamples(tt.examples) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr) diff --git a/internal/render/inline.go b/internal/render/inline.go index d90a967..8d08ed8 100644 --- a/internal/render/inline.go +++ b/internal/render/inline.go @@ -23,7 +23,6 @@ type inlineSegment struct { // When no backticks are present, // it falls through to the single-segment fast path (applyStyle + wrapText). func (r *Renderer) renderStyledInline( - w io.Writer, text, indent string, baseStyle, @@ -32,7 +31,7 @@ func (r *Renderer) renderStyledInline( segments := parseInline(text) if len(segments) == 1 { _, err := io.WriteString( - w, + r.w, r.applyStyle( baseStyle, r.wrapText(text, indent), @@ -45,14 +44,13 @@ func (r *Renderer) renderStyledInline( for i, line := range wrappedLines { if i > 0 { - _, err := io.WriteString(w, "\n") + _, err := io.WriteString(r.w, "\n") if err != nil { return err } } if err := r.renderStyledLine( - w, line, segments, baseStyle, @@ -86,7 +84,6 @@ func (r *Renderer) wrapInlineSegments( // Segments are matched to the line text // by position to avoid false matches from repeated words. func (r *Renderer) renderStyledLine( - w io.Writer, line string, segments []inlineSegment, baseStyle, @@ -114,7 +111,7 @@ func (r *Renderer) renderStyledLine( before := remaining[:pos] remaining = remaining[pos:] _, err := io.WriteString( - w, + r.w, r.applyStyle(baseStyle, before), ) if err != nil { @@ -127,19 +124,19 @@ func (r *Renderer) renderStyledLine( piece := remaining[:len(segment.text)] remaining = remaining[len(segment.text):] if segment.code { - _, err := io.WriteString(w, r.applyStyle(codeStyle, piece)) + _, err := io.WriteString(r.w, r.applyStyle(codeStyle, piece)) if err != nil { return err } } else { - _, err := io.WriteString(w, r.applyStyle(baseStyle, piece)) + _, err := io.WriteString(r.w, r.applyStyle(baseStyle, piece)) if err != nil { return err } } } if remaining != "" { - _, err := io.WriteString(w, r.applyStyle(baseStyle, remaining)) + _, err := io.WriteString(r.w, r.applyStyle(baseStyle, remaining)) if err != nil { return err } diff --git a/internal/render/inline_test.go b/internal/render/inline_test.go index e88b852..818091e 100644 --- a/internal/render/inline_test.go +++ b/internal/render/inline_test.go @@ -96,7 +96,10 @@ func TestRenderStyledInline(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: tt.useColor, output: config.OutputConfig{LineLength: tt.lineLength}, style: config.StyleConfig{ @@ -105,14 +108,11 @@ func TestRenderStyledInline(t *testing.T) { }, } - var buf strings.Builder - w := io.Writer(&buf) if tt.writer != nil { - w = tt.writer + r.w = tt.writer } err := r.renderStyledInline( - w, tt.text, tt.indent, r.style.Description, @@ -292,16 +292,16 @@ func TestRenderStyledLine(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - r := &Renderer{} - var buf strings.Builder w := io.Writer(&buf) + r := &Renderer{ + w: w, + } if tt.writer != nil { - w = tt.writer + r.w = tt.writer } err := r.renderStyledLine( - w, tt.line, tt.segments, config.OutputStyle{}, diff --git a/internal/render/render.go b/internal/render/render.go index 7fcd218..ad9beeb 100644 --- a/internal/render/render.go +++ b/internal/render/render.go @@ -117,11 +117,11 @@ func (r *Renderer) Render(platform string, p *Page) error { return err } - if err := r.renderDescriptions(r.w, p.Description, p.URL); err != nil { + if err := r.renderDescriptions(p.Description, p.URL); err != nil { return err } - if err := r.renderExamples(r.w, p.Examples); err != nil { + if err := r.renderExamples(p.Examples); err != nil { return err } diff --git a/internal/render/title.go b/internal/render/title.go index bfba820..4704bd8 100644 --- a/internal/render/title.go +++ b/internal/render/title.go @@ -8,10 +8,10 @@ import ( // renderTitle writes the page title, // styled with r.style.Title and // indented by r.indent.Title spaces. -func (r *Renderer) renderTitle(w io.Writer, title string) error { +func (r *Renderer) renderTitle(title string) error { indent := strings.Repeat(" ", r.indent.Title) _, err := io.WriteString( - w, + r.w, r.applyStyle( r.style.Title, r.wrapText(title, indent), @@ -39,7 +39,7 @@ func (r *Renderer) renderPageTitle(platform string, p *Page) error { title = platform + "/" + title } - if err := r.renderTitle(r.w, title); err != nil { + if err := r.renderTitle(title); err != nil { return err } diff --git a/internal/render/title_test.go b/internal/render/title_test.go index be4f81e..a6e6845 100644 --- a/internal/render/title_test.go +++ b/internal/render/title_test.go @@ -56,19 +56,20 @@ func TestRenderTitle(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var buf strings.Builder + w := io.Writer(&buf) r := &Renderer{ + w: w, useColor: tt.useColor, style: config.StyleConfig{Title: tt.style}, indent: config.IndentConfig{Title: tt.indent}, } - var buf strings.Builder - w := io.Writer(&buf) if tt.writer != nil { - w = tt.writer + r.w = tt.writer } - err := r.renderTitle(w, tt.title) + err := r.renderTitle(tt.title) if tt.wantErr != "" { assert.ErrorContains(t, err, tt.wantErr)