Skip to content

Commit ca4dd04

Browse files
committed
Fix Markdown Alignment Update #244
1 parent 8596f56 commit ca4dd04

File tree

5 files changed

+222
-112
lines changed

5 files changed

+222
-112
lines changed

config.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -929,6 +929,15 @@ func WithTrimSpace(state tw.State) Option {
929929
}
930930
}
931931

932+
// WithAlignment helps to set default alignments
933+
func WithAlignment(alignment tw.Alignment) Option {
934+
return func(target *Table) {
935+
target.config.Header.ColumnAligns = alignment
936+
target.config.Row.ColumnAligns = alignment
937+
target.config.Footer.ColumnAligns = alignment
938+
}
939+
}
940+
932941
// defaultConfig returns a default Config with sensible settings for headers, rows, footers, and behavior.
933942
func defaultConfig() Config {
934943
defaultPadding := tw.Padding{Left: tw.Space, Right: tw.Space, Top: tw.Empty, Bottom: tw.Empty}

renderer/markdown.go

Lines changed: 106 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ import (
1111

1212
// Markdown renders tables in Markdown format with customizable settings.
1313
type Markdown struct {
14-
config tw.Rendition // Rendering configuration
15-
logger *ll.Logger // Debug trace messages
14+
config tw.Rendition // Rendering configuration
15+
logger *ll.Logger // Debug trace messages
16+
alignment tw.Alignment // alias of []tw.Align
1617
}
1718

1819
// NewMarkdown initializes a Markdown renderer with defaults tailored for Markdown (e.g., pipes, header separator).
@@ -65,6 +66,93 @@ func (m *Markdown) Config() tw.Rendition {
6566
return m.config
6667
}
6768

69+
// Header renders the Markdown table header and its separator line.
70+
func (m *Markdown) Header(w io.Writer, headers [][]string, ctx tw.Formatting) {
71+
m.resolveAlignment(ctx)
72+
if len(headers) == 0 || len(headers[0]) == 0 {
73+
m.logger.Debug("Header: No headers to render")
74+
return
75+
}
76+
m.logger.Debug("Rendering header with %d lines, widths=%v, current=%v, next=%v",
77+
len(headers), ctx.Row.Widths, ctx.Row.Current, ctx.Row.Next)
78+
79+
// Render header content
80+
m.renderMarkdownLine(w, headers[0], ctx, false)
81+
82+
// Render separator if enabled
83+
if m.config.Settings.Lines.ShowHeaderLine.Enabled() {
84+
sepCtx := ctx
85+
sepCtx.Row.Widths = ctx.Row.Widths
86+
sepCtx.Row.Current = ctx.Row.Current
87+
sepCtx.Row.Previous = ctx.Row.Current
88+
sepCtx.IsSubRow = true
89+
m.renderMarkdownLine(w, nil, sepCtx, true)
90+
}
91+
}
92+
93+
// Row renders a Markdown table data row.
94+
func (m *Markdown) Row(w io.Writer, row []string, ctx tw.Formatting) {
95+
m.resolveAlignment(ctx)
96+
m.logger.Debug("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v",
97+
row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
98+
m.renderMarkdownLine(w, row, ctx, false)
99+
100+
}
101+
102+
// Footer renders the Markdown table footer.
103+
func (m *Markdown) Footer(w io.Writer, footers [][]string, ctx tw.Formatting) {
104+
m.resolveAlignment(ctx)
105+
if len(footers) == 0 || len(footers[0]) == 0 {
106+
m.logger.Debug("Footer: No footers to render")
107+
return
108+
}
109+
m.logger.Debug("Rendering footer with %d lines, widths=%v, previous=%v, current=%v, next=%v",
110+
len(footers), ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
111+
m.renderMarkdownLine(w, footers[0], ctx, false)
112+
}
113+
114+
// Line is a no-op for Markdown, as only the header separator is rendered (handled by Header).
115+
func (m *Markdown) Line(w io.Writer, ctx tw.Formatting) {
116+
m.logger.Debug("Line: Generic Line call received (pos: %s, loc: %s). Markdown ignores these.", ctx.Row.Position, ctx.Row.Location)
117+
}
118+
119+
// Reset clears the renderer's internal state, including debug traces.
120+
func (m *Markdown) Reset() {
121+
m.logger.Info("Reset: Cleared debug trace")
122+
}
123+
124+
func (m *Markdown) Start(w io.Writer) error {
125+
m.logger.Warn("Markdown.Start() called (no-op).")
126+
return nil
127+
}
128+
129+
func (m *Markdown) Close(w io.Writer) error {
130+
m.logger.Warn("Markdown.Close() called (no-op).")
131+
return nil
132+
}
133+
134+
func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
135+
if len(m.alignment) != 0 {
136+
return m.alignment
137+
}
138+
139+
// get total columns
140+
total := len(ctx.Row.Current)
141+
142+
// build default alignment
143+
for i := 0; i < total; i++ {
144+
m.alignment = append(m.alignment, tw.AlignLeft)
145+
}
146+
147+
// add per colum alignment if it exits
148+
for i := 0; i < total; i++ {
149+
m.alignment[i] = ctx.Row.Current[i].Align
150+
}
151+
152+
m.logger.Debug(" → Alignment Resolved %s", m.alignment)
153+
return m.alignment
154+
}
155+
68156
// formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide.
69157
func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string {
70158
//if m.config.Settings.TrimWhitespace.Enabled() {
@@ -154,7 +242,7 @@ func (m *Markdown) formatCell(content string, width int, align tw.Align, padding
154242

155243
// formatSeparator generates a Markdown separator (e.g., `---`, `:--`, `:-:`) with alignment indicators.
156244
func (m *Markdown) formatSeparator(width int, align tw.Align) string {
157-
targetWidth := tw.Max(3, width) // Minimum width of 3
245+
targetWidth := tw.Max(3, width)
158246
var sb strings.Builder
159247

160248
switch align {
@@ -168,7 +256,7 @@ func (m *Markdown) formatSeparator(width int, align tw.Align) string {
168256
sb.WriteRune(':')
169257
sb.WriteString(strings.Repeat("-", targetWidth-2))
170258
sb.WriteRune(':')
171-
default: // Fallback to left alignment for unspecified
259+
default:
172260
sb.WriteRune(':')
173261
sb.WriteString(strings.Repeat("-", targetWidth-1))
174262
}
@@ -230,17 +318,13 @@ func (m *Markdown) renderMarkdownLine(w io.Writer, line []string, ctx tw.Formatt
230318

231319
for colIndex < numCols {
232320
cellCtx, ok := ctx.Row.Current[colIndex]
321+
align := m.alignment[colIndex]
322+
233323
defaultPadding := tw.Padding{Left: " ", Right: " "}
234324
if !ok {
235-
defaultAlign := tw.AlignLeft // Default for rows
236-
if ctx.Row.Position == tw.Header && !isHeaderSep {
237-
defaultAlign = tw.AlignCenter // Default for headers
238-
}
239-
if ctx.Row.Position == tw.Footer {
240-
defaultAlign = tw.AlignRight
241-
}
325+
242326
cellCtx = tw.CellContext{
243-
Data: "", Align: defaultAlign, Padding: defaultPadding,
327+
Data: "", Align: align, Padding: defaultPadding,
244328
Width: ctx.Row.Widths.Get(colIndex), Merge: tw.MergeState{},
245329
}
246330
} else if cellCtx.Padding == (tw.Padding{}) {
@@ -256,7 +340,7 @@ func (m *Markdown) renderMarkdownLine(w io.Writer, line []string, ctx tw.Formatt
256340

257341
// Calculate width and span
258342
span := 1
259-
align := cellCtx.Align
343+
260344
if align == tw.AlignNone || align == "" {
261345
if ctx.Row.Position == tw.Header && !isHeaderSep {
262346
align = tw.AlignCenter
@@ -299,26 +383,28 @@ func (m *Markdown) renderMarkdownLine(w io.Writer, line []string, ctx tw.Formatt
299383
} else {
300384
var formattedSegment string
301385
if isHeaderSep {
302-
// Use the header's alignment from ctx.Row.Previous (header row)
386+
// Use header's alignment from ctx.Row.Previous
303387
headerAlign := tw.AlignCenter // Default for headers
304388
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK {
305389
headerAlign = headerCellCtx.Align
306390
if headerAlign == tw.AlignNone || headerAlign == "" {
307391
headerAlign = tw.AlignCenter
308392
}
309-
} else if cellCtx, ok := ctx.Row.Current[colIndex]; ok {
310-
headerAlign = cellCtx.Align
311-
if headerAlign == tw.AlignNone || headerAlign == "" {
312-
headerAlign = tw.AlignCenter
313-
}
314393
}
315394
formattedSegment = m.formatSeparator(visualWidth, headerAlign)
316395
} else {
317396
content := ""
318397
if colIndex < len(line) {
319398
content = line[colIndex]
320399
}
321-
formattedSegment = m.formatCell(content, visualWidth, align, cellCtx.Padding)
400+
// For rows, use the header's alignment if specified
401+
rowAlign := align
402+
if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false {
403+
if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != "" {
404+
rowAlign = headerCellCtx.Align
405+
}
406+
}
407+
formattedSegment = m.formatCell(content, visualWidth, rowAlign, cellCtx.Padding)
322408
}
323409
output.WriteString(formattedSegment)
324410
m.logger.Debug("renderMarkdownLine: Wrote col %d (span %d, width %d): '%s'", colIndex, span, visualWidth, formattedSegment)
@@ -332,64 +418,3 @@ func (m *Markdown) renderMarkdownLine(w io.Writer, line []string, ctx tw.Formatt
332418
fmt.Fprint(w, output.String())
333419
m.logger.Debug("renderMarkdownLine: Final line: %s", strings.TrimSuffix(output.String(), tw.NewLine))
334420
}
335-
336-
// Header renders the Markdown table header and its separator line.
337-
func (m *Markdown) Header(w io.Writer, headers [][]string, ctx tw.Formatting) {
338-
if len(headers) == 0 || len(headers[0]) == 0 {
339-
m.logger.Debug("Header: No headers to render")
340-
return
341-
}
342-
m.logger.Debug("Rendering header with %d lines, widths=%v, current=%v, next=%v",
343-
len(headers), ctx.Row.Widths, ctx.Row.Current, ctx.Row.Next)
344-
345-
// Render header content
346-
m.renderMarkdownLine(w, headers[0], ctx, false)
347-
348-
// Render separator if enabled
349-
if m.config.Settings.Lines.ShowHeaderLine.Enabled() {
350-
sepCtx := ctx
351-
sepCtx.Row.Widths = ctx.Row.Widths
352-
sepCtx.Row.Current = ctx.Row.Current
353-
sepCtx.Row.Previous = ctx.Row.Current
354-
sepCtx.IsSubRow = true
355-
m.renderMarkdownLine(w, nil, sepCtx, true)
356-
}
357-
}
358-
359-
// Row renders a Markdown table data row.
360-
func (m *Markdown) Row(w io.Writer, row []string, ctx tw.Formatting) {
361-
m.logger.Debug("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v",
362-
row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
363-
m.renderMarkdownLine(w, row, ctx, false)
364-
}
365-
366-
// Footer renders the Markdown table footer.
367-
func (m *Markdown) Footer(w io.Writer, footers [][]string, ctx tw.Formatting) {
368-
if len(footers) == 0 || len(footers[0]) == 0 {
369-
m.logger.Debug("Footer: No footers to render")
370-
return
371-
}
372-
m.logger.Debug("Rendering footer with %d lines, widths=%v, previous=%v, current=%v, next=%v",
373-
len(footers), ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
374-
m.renderMarkdownLine(w, footers[0], ctx, false)
375-
}
376-
377-
// Line is a no-op for Markdown, as only the header separator is rendered (handled by Header).
378-
func (m *Markdown) Line(w io.Writer, ctx tw.Formatting) {
379-
m.logger.Debug("Line: Generic Line call received (pos: %s, loc: %s). Markdown ignores these.", ctx.Row.Position, ctx.Row.Location)
380-
}
381-
382-
// Reset clears the renderer's internal state, including debug traces.
383-
func (m *Markdown) Reset() {
384-
m.logger.Info("Reset: Cleared debug trace")
385-
}
386-
387-
func (m *Markdown) Start(w io.Writer) error {
388-
m.logger.Warn("Markdown.Start() called (no-op).")
389-
return nil
390-
}
391-
392-
func (m *Markdown) Close(w io.Writer) error {
393-
m.logger.Warn("Markdown.Close() called (no-op).")
394-
return nil
395-
}

0 commit comments

Comments
 (0)