File: markdown.go

package info (click to toggle)
golang-github-olekukonko-tablewriter 1.0.9-1
  • links: PTS, VCS
  • area: main
  • in suites: experimental, forky, sid
  • size: 1,380 kB
  • sloc: makefile: 4
file content (419 lines) | stat: -rw-r--r-- 13,906 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
package renderer

import (
	"github.com/olekukonko/ll"
	"github.com/olekukonko/tablewriter/pkg/twwidth"
	"io"
	"strings"

	"github.com/olekukonko/tablewriter/tw"
)

// Markdown renders tables in Markdown format with customizable settings.
type Markdown struct {
	config    tw.Rendition // Rendering configuration
	logger    *ll.Logger   // Debug trace messages
	alignment tw.Alignment // alias of []tw.Align
	w         io.Writer
}

// NewMarkdown initializes a Markdown renderer with defaults tailored for Markdown (e.g., pipes, header separator).
// Only the first config is used if multiple are provided.
func NewMarkdown(configs ...tw.Rendition) *Markdown {
	cfg := defaultBlueprint()
	// Configure Markdown-specific defaults
	cfg.Symbols = tw.NewSymbols(tw.StyleMarkdown)
	cfg.Borders = tw.Border{Left: tw.On, Right: tw.On, Top: tw.Off, Bottom: tw.Off}
	cfg.Settings.Separators.BetweenColumns = tw.On
	cfg.Settings.Separators.BetweenRows = tw.Off
	cfg.Settings.Lines.ShowHeaderLine = tw.On
	cfg.Settings.Lines.ShowTop = tw.Off
	cfg.Settings.Lines.ShowBottom = tw.Off
	cfg.Settings.Lines.ShowFooterLine = tw.Off
	// cfg.Settings.TrimWhitespace = tw.On

	// Apply user overrides
	if len(configs) > 0 {
		cfg = mergeMarkdownConfig(cfg, configs[0])
	}
	return &Markdown{config: cfg, logger: ll.New("markdown")}
}

// mergeMarkdownConfig combines user-provided config with Markdown defaults, enforcing Markdown-specific settings.
func mergeMarkdownConfig(defaults, overrides tw.Rendition) tw.Rendition {
	if overrides.Borders.Left != 0 {
		defaults.Borders.Left = overrides.Borders.Left
	}
	if overrides.Borders.Right != 0 {
		defaults.Borders.Right = overrides.Borders.Right
	}
	if overrides.Symbols != nil {
		defaults.Symbols = overrides.Symbols
	}
	defaults.Settings = mergeSettings(defaults.Settings, overrides.Settings)
	// Enforce Markdown requirements
	defaults.Settings.Lines.ShowHeaderLine = tw.On
	defaults.Settings.Separators.BetweenColumns = tw.On
	// defaults.Settings.TrimWhitespace = tw.On
	return defaults
}

func (m *Markdown) Logger(logger *ll.Logger) {
	m.logger = logger.Namespace("markdown")
}

// Config returns the renderer's current configuration.
func (m *Markdown) Config() tw.Rendition {
	return m.config
}

// Header renders the Markdown table header and its separator line.
func (m *Markdown) Header(headers [][]string, ctx tw.Formatting) {
	m.resolveAlignment(ctx)
	if len(headers) == 0 || len(headers[0]) == 0 {
		m.logger.Debug("Header: No headers to render")
		return
	}
	m.logger.Debugf("Rendering header with %d lines, widths=%v, current=%v, next=%v", len(headers), ctx.Row.Widths, ctx.Row.Current, ctx.Row.Next)

	// Render header content
	m.renderMarkdownLine(headers[0], ctx, false)

	// Render separator if enabled
	if m.config.Settings.Lines.ShowHeaderLine.Enabled() {
		sepCtx := ctx
		sepCtx.Row.Widths = ctx.Row.Widths
		sepCtx.Row.Current = ctx.Row.Current
		sepCtx.Row.Previous = ctx.Row.Current
		sepCtx.IsSubRow = true
		m.renderMarkdownLine(nil, sepCtx, true)
	}
}

// Row renders a Markdown table data row.
func (m *Markdown) Row(row []string, ctx tw.Formatting) {
	m.resolveAlignment(ctx)
	m.logger.Debugf("Rendering row with data=%v, widths=%v, previous=%v, current=%v, next=%v", row, ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
	m.renderMarkdownLine(row, ctx, false)

}

// Footer renders the Markdown table footer.
func (m *Markdown) Footer(footers [][]string, ctx tw.Formatting) {
	m.resolveAlignment(ctx)
	if len(footers) == 0 || len(footers[0]) == 0 {
		m.logger.Debug("Footer: No footers to render")
		return
	}
	m.logger.Debugf("Rendering footer with %d lines, widths=%v, previous=%v, current=%v, next=%v",
		len(footers), ctx.Row.Widths, ctx.Row.Previous, ctx.Row.Current, ctx.Row.Next)
	m.renderMarkdownLine(footers[0], ctx, false)
}

// Line is a no-op for Markdown, as only the header separator is rendered (handled by Header).
func (m *Markdown) Line(ctx tw.Formatting) {
	m.logger.Debugf("Line: Generic Line call received (pos: %s, loc: %s). Markdown ignores these.", ctx.Row.Position, ctx.Row.Location)
}

// Reset clears the renderer's internal state, including debug traces.
func (m *Markdown) Reset() {
	m.logger.Info("Reset: Cleared debug trace")
}

func (m *Markdown) Start(w io.Writer) error {
	m.w = w
	m.logger.Warn("Markdown.Start() called (no-op).")
	return nil
}

func (m *Markdown) Close() error {
	m.logger.Warn("Markdown.Close() called (no-op).")
	return nil
}

func (m *Markdown) resolveAlignment(ctx tw.Formatting) tw.Alignment {
	if len(m.alignment) != 0 {
		return m.alignment
	}

	// get total columns
	total := len(ctx.Row.Current)

	// build default alignment
	for i := 0; i < total; i++ {
		m.alignment = append(m.alignment, tw.AlignNone) // Default to AlignNone
	}

	// add per column alignment if it exists
	for i := 0; i < total; i++ {
		m.alignment[i] = ctx.Row.Current[i].Align
	}

	m.logger.Debugf(" → Align Resolved %s", m.alignment)
	return m.alignment
}

// formatCell formats a Markdown cell's content with padding and alignment, ensuring at least 3 characters wide.
func (m *Markdown) formatCell(content string, width int, align tw.Align, padding tw.Padding) string {
	//if m.config.Settings.TrimWhitespace.Enabled() {
	//	content = strings.TrimSpace(content)
	//}
	contentVisualWidth := twwidth.Width(content)

	// Use specified padding characters or default to spaces
	padLeftChar := padding.Left
	if padLeftChar == tw.Empty {
		padLeftChar = tw.Space
	}
	padRightChar := padding.Right
	if padRightChar == tw.Empty {
		padRightChar = tw.Space
	}

	// Calculate padding widths
	padLeftCharWidth := twwidth.Width(padLeftChar)
	padRightCharWidth := twwidth.Width(padRightChar)
	minWidth := tw.Max(3, contentVisualWidth+padLeftCharWidth+padRightCharWidth)
	targetWidth := tw.Max(width, minWidth)

	// Calculate padding
	totalPaddingNeeded := targetWidth - contentVisualWidth
	if totalPaddingNeeded < 0 {
		totalPaddingNeeded = 0
	}

	var leftPadStr, rightPadStr string
	switch align {
	case tw.AlignRight:
		leftPadCount := tw.Max(0, totalPaddingNeeded-padRightCharWidth)
		rightPadCount := totalPaddingNeeded - leftPadCount
		leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
		rightPadStr = strings.Repeat(padRightChar, rightPadCount)
	case tw.AlignCenter:
		leftPadCount := totalPaddingNeeded / 2
		rightPadCount := totalPaddingNeeded - leftPadCount
		if leftPadCount < padLeftCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth {
			leftPadCount = padLeftCharWidth
			rightPadCount = totalPaddingNeeded - leftPadCount
		}
		if rightPadCount < padRightCharWidth && totalPaddingNeeded >= padLeftCharWidth+padRightCharWidth {
			rightPadCount = padRightCharWidth
			leftPadCount = totalPaddingNeeded - rightPadCount
		}
		leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
		rightPadStr = strings.Repeat(padRightChar, rightPadCount)
	default: // AlignLeft
		rightPadCount := tw.Max(0, totalPaddingNeeded-padLeftCharWidth)
		leftPadCount := totalPaddingNeeded - rightPadCount
		leftPadStr = strings.Repeat(padLeftChar, leftPadCount)
		rightPadStr = strings.Repeat(padRightChar, rightPadCount)
	}

	// Build result
	result := leftPadStr + content + rightPadStr

	// Adjust width if needed
	finalWidth := twwidth.Width(result)
	if finalWidth != targetWidth {
		m.logger.Debugf("Markdown formatCell MISMATCH: content='%s', target_w=%d, paddingL='%s', paddingR='%s', align=%s -> result='%s', result_w=%d",
			content, targetWidth, padding.Left, padding.Right, align, result, finalWidth)
		adjNeeded := targetWidth - finalWidth
		if adjNeeded > 0 {
			adjStr := strings.Repeat(tw.Space, adjNeeded)
			if align == tw.AlignRight {
				result = adjStr + result
			} else if align == tw.AlignCenter {
				leftAdj := adjNeeded / 2
				rightAdj := adjNeeded - leftAdj
				result = strings.Repeat(tw.Space, leftAdj) + result + strings.Repeat(tw.Space, rightAdj)
			} else {
				result += adjStr
			}
		} else {
			result = twwidth.Truncate(result, targetWidth)
		}
		m.logger.Debugf("Markdown formatCell Corrected: target_w=%d, result='%s', new_w=%d", targetWidth, result, twwidth.Width(result))
	}

	m.logger.Debugf("Markdown formatCell: content='%s', width=%d, align=%s, paddingL='%s', paddingR='%s' -> '%s' (target %d)",
		content, width, align, padding.Left, padding.Right, result, targetWidth)
	return result
}

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

	switch align {
	case tw.AlignLeft:
		sb.WriteRune(':')
		sb.WriteString(strings.Repeat("-", targetWidth-1))
	case tw.AlignRight:
		sb.WriteString(strings.Repeat("-", targetWidth-1))
		sb.WriteRune(':')
	case tw.AlignCenter:
		sb.WriteRune(':')
		sb.WriteString(strings.Repeat("-", targetWidth-2))
		sb.WriteRune(':')
	case tw.AlignNone:
		sb.WriteString(strings.Repeat("-", targetWidth))
	default:
		sb.WriteString(strings.Repeat("-", targetWidth)) // Fallback
	}

	result := sb.String()
	currentLen := twwidth.Width(result)
	if currentLen < targetWidth {
		result += strings.Repeat("-", targetWidth-currentLen)
	} else if currentLen > targetWidth {
		result = twwidth.Truncate(result, targetWidth)
	}

	m.logger.Debugf("Markdown formatSeparator: width=%d, align=%s -> '%s'", width, align, result)
	return result
}

// renderMarkdownLine renders a single Markdown line (header, row, footer, or separator) with pipes and alignment.
func (m *Markdown) renderMarkdownLine(line []string, ctx tw.Formatting, isHeaderSep bool) {
	numCols := 0
	if len(ctx.Row.Widths) > 0 {
		maxKey := -1
		for k := range ctx.Row.Widths {
			if k > maxKey {
				maxKey = k
			}
		}
		numCols = maxKey + 1
	} else if len(ctx.Row.Current) > 0 {
		maxKey := -1
		for k := range ctx.Row.Current {
			if k > maxKey {
				maxKey = k
			}
		}
		numCols = maxKey + 1
	} else if len(line) > 0 && !isHeaderSep {
		numCols = len(line)
	}

	if numCols == 0 && !isHeaderSep {
		m.logger.Debug("renderMarkdownLine: Skipping line with zero columns.")
		return
	}

	var output strings.Builder
	prefix := m.config.Symbols.Column()
	if m.config.Borders.Left == tw.Off {
		prefix = tw.Empty
	}
	suffix := m.config.Symbols.Column()
	if m.config.Borders.Right == tw.Off {
		suffix = tw.Empty
	}
	separator := m.config.Symbols.Column()
	output.WriteString(prefix)

	colIndex := 0
	separatorWidth := twwidth.Width(separator)

	for colIndex < numCols {
		cellCtx, ok := ctx.Row.Current[colIndex]
		align := m.alignment[colIndex]

		defaultPadding := tw.Padding{Left: tw.Space, Right: tw.Space}
		if !ok {
			cellCtx = tw.CellContext{
				Data: tw.Empty, Align: align, Padding: defaultPadding,
				Width: ctx.Row.Widths.Get(colIndex), Merge: tw.MergeState{},
			}
		} else if !cellCtx.Padding.Paddable() {
			cellCtx.Padding = defaultPadding
		}

		// Add separator
		isContinuation := ok && cellCtx.Merge.Horizontal.Present && !cellCtx.Merge.Horizontal.Start
		if colIndex > 0 && !isContinuation {
			output.WriteString(separator)
			m.logger.Debugf("renderMarkdownLine: Added separator '%s' before col %d", separator, colIndex)
		}

		// Calculate width and span
		span := 1
		visualWidth := 0
		isHMergeStart := ok && cellCtx.Merge.Horizontal.Present && cellCtx.Merge.Horizontal.Start
		if isHMergeStart {
			span = cellCtx.Merge.Horizontal.Span
			totalWidth := 0
			for k := 0; k < span && colIndex+k < numCols; k++ {
				colWidth := ctx.NormalizedWidths.Get(colIndex + k)
				if colWidth < 0 {
					colWidth = 0
				}
				totalWidth += colWidth
				if k > 0 && separatorWidth > 0 {
					totalWidth += separatorWidth
				}
			}
			visualWidth = totalWidth
			m.logger.Debugf("renderMarkdownLine: HMerge col %d, span %d, visualWidth %d", colIndex, span, visualWidth)
		} else {
			visualWidth = ctx.Row.Widths.Get(colIndex)
			m.logger.Debugf("renderMarkdownLine: Regular col %d, visualWidth %d", colIndex, visualWidth)
		}
		if visualWidth < 0 {
			visualWidth = 0
		}

		// Render segment
		if isContinuation {
			m.logger.Debugf("renderMarkdownLine: Skipping col %d (HMerge continuation)", colIndex)
		} else {
			var formattedSegment string
			if isHeaderSep {
				// Use header's alignment from ctx.Row.Previous
				headerAlign := align
				if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK {
					headerAlign = headerCellCtx.Align
					// Preserve tw.AlignNone for separator
					if headerAlign != tw.AlignNone && (headerAlign == tw.Empty || headerAlign == tw.Skip) {
						headerAlign = tw.AlignCenter
					}
				}
				formattedSegment = m.formatSeparator(visualWidth, headerAlign)
			} else {
				content := tw.Empty
				if colIndex < len(line) {
					content = line[colIndex]
				}
				// For rows, use the header's alignment if specified
				rowAlign := align
				if headerCellCtx, headerOK := ctx.Row.Previous[colIndex]; headerOK && isHeaderSep == false {
					if headerCellCtx.Align != tw.AlignNone && headerCellCtx.Align != tw.Empty {
						rowAlign = headerCellCtx.Align
					}
				}
				if rowAlign == tw.AlignNone || rowAlign == tw.Empty {
					if ctx.Row.Position == tw.Header {
						rowAlign = tw.AlignCenter
					} else if ctx.Row.Position == tw.Footer {
						rowAlign = tw.AlignRight
					} else {
						rowAlign = tw.AlignLeft
					}
					m.logger.Debugf("renderMarkdownLine: Col %d using default align '%s'", colIndex, rowAlign)
				}
				formattedSegment = m.formatCell(content, visualWidth, rowAlign, cellCtx.Padding)
			}
			output.WriteString(formattedSegment)
			m.logger.Debugf("renderMarkdownLine: Wrote col %d (span %d, width %d): '%s'", colIndex, span, visualWidth, formattedSegment)
		}

		colIndex += span
	}

	output.WriteString(suffix)
	output.WriteString(tw.NewLine)
	m.w.Write([]byte(output.String()))
	m.logger.Debugf("renderMarkdownLine: Final line: %s", strings.TrimSuffix(output.String(), tw.NewLine))
}