-
-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathmarkdown.go
More file actions
709 lines (631 loc) · 22.5 KB
/
markdown.go
File metadata and controls
709 lines (631 loc) · 22.5 KB
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
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
// Package markdown is markdown builder that includes to convert Markdown to HTML.
package markdown
import (
"errors"
"fmt"
"io"
"strings"
"unicode"
"github.com/nao1215/markdown/internal"
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/renderer"
"github.com/olekukonko/tablewriter/tw"
)
// SyntaxHighlight is syntax highlight language.
type SyntaxHighlight string
const (
// SyntaxHighlightNone is no syntax highlight.
SyntaxHighlightNone SyntaxHighlight = ""
// SyntaxHighlightText is syntax highlight for text.
SyntaxHighlightText SyntaxHighlight = "text"
// SyntaxHighlightAPIBlueprint is syntax highlight for API Blueprint.
SyntaxHighlightAPIBlueprint SyntaxHighlight = "markdown"
// SyntaxHighlightShell is syntax highlight for Shell.
SyntaxHighlightShell SyntaxHighlight = "shell"
// SyntaxHighlightGo is syntax highlight for Go.
SyntaxHighlightGo SyntaxHighlight = "go"
// SyntaxHighlightJSON is syntax highlight for JSON.
SyntaxHighlightJSON SyntaxHighlight = "json"
// SyntaxHighlightYAML is syntax highlight for YAML.
SyntaxHighlightYAML SyntaxHighlight = "yaml"
// SyntaxHighlightXML is syntax highlight for XML.
SyntaxHighlightXML SyntaxHighlight = "xml"
// SyntaxHighlightHTML is syntax highlight for HTML.
SyntaxHighlightHTML SyntaxHighlight = "html"
// SyntaxHighlightCSS is syntax highlight for CSS.
SyntaxHighlightCSS SyntaxHighlight = "css"
// SyntaxHighlightJavaScript is syntax highlight for JavaScript.
SyntaxHighlightJavaScript SyntaxHighlight = "javascript"
// SyntaxHighlightTypeScript is syntax highlight for TypeScript.
SyntaxHighlightTypeScript SyntaxHighlight = "typescript"
// SyntaxHighlightSQL is syntax highlight for SQL.
SyntaxHighlightSQL SyntaxHighlight = "sql"
// SyntaxHighlightC is syntax highlight for C.
SyntaxHighlightC SyntaxHighlight = "c"
// SyntaxHighlightCSharp is syntax highlight for C#.
SyntaxHighlightCSharp SyntaxHighlight = "csharp"
// SyntaxHighlightCPlusPlus is syntax highlight for C++.
SyntaxHighlightCPlusPlus SyntaxHighlight = "cpp"
// SyntaxHighlightJava is syntax highlight for Java.
SyntaxHighlightJava SyntaxHighlight = "java"
// SyntaxHighlightKotlin is syntax highlight for Kotlin.
SyntaxHighlightKotlin SyntaxHighlight = "kotlin"
// SyntaxHighlightPHP is syntax highlight for PHP.
SyntaxHighlightPHP SyntaxHighlight = "php"
// SyntaxHighlightPython is syntax highlight for Python.
SyntaxHighlightPython SyntaxHighlight = "python"
// SyntaxHighlightRuby is syntax highlight for Ruby.
SyntaxHighlightRuby SyntaxHighlight = "ruby"
// SyntaxHighlightSwift is syntax highlight for Swift.
SyntaxHighlightSwift SyntaxHighlight = "swift"
// SyntaxHighlightScala is syntax highlight for Scala.
SyntaxHighlightScala SyntaxHighlight = "scala"
// SyntaxHighlightRust is syntax highlight for Rust.
SyntaxHighlightRust SyntaxHighlight = "rust"
// SyntaxHighlightObjectiveC is syntax highlight for Objective-C.
SyntaxHighlightObjectiveC SyntaxHighlight = "objectivec"
// SyntaxHighlightPerl is syntax highlight for Perl.
SyntaxHighlightPerl SyntaxHighlight = "perl"
// SyntaxHighlightLua is syntax highlight for Lua.
SyntaxHighlightLua SyntaxHighlight = "lua"
// SyntaxHighlightDart is syntax highlight for Dart.
SyntaxHighlightDart SyntaxHighlight = "dart"
// SyntaxHighlightClojure is syntax highlight for Clojure.
SyntaxHighlightClojure SyntaxHighlight = "clojure"
// SyntaxHighlightGroovy is syntax highlight for Groovy.
SyntaxHighlightGroovy SyntaxHighlight = "groovy"
// SyntaxHighlightR is syntax highlight for R.
SyntaxHighlightR SyntaxHighlight = "r"
// SyntaxHighlightHaskell is syntax highlight for Haskell.
SyntaxHighlightHaskell SyntaxHighlight = "haskell"
// SyntaxHighlightErlang is syntax highlight for Erlang.
SyntaxHighlightErlang SyntaxHighlight = "erlang"
// SyntaxHighlightElixir is syntax highlight for Elixir.
SyntaxHighlightElixir SyntaxHighlight = "elixir"
// SyntaxHighlightOCaml is syntax highlight for OCaml.
SyntaxHighlightOCaml SyntaxHighlight = "ocaml"
// SyntaxHighlightJulia is syntax highlight for Julia.
SyntaxHighlightJulia SyntaxHighlight = "julia"
// SyntaxHighlightScheme is syntax highlight for Scheme.
SyntaxHighlightScheme SyntaxHighlight = "scheme"
// SyntaxHighlightFSharp is syntax highlight for F#.
SyntaxHighlightFSharp SyntaxHighlight = "fsharp"
// SyntaxHighlightCoffeeScript is syntax highlight for CoffeeScript.
SyntaxHighlightCoffeeScript SyntaxHighlight = "coffeescript"
// SyntaxHighlightVBNet is syntax highlight for VB.NET.
SyntaxHighlightVBNet SyntaxHighlight = "vbnet"
// SyntaxHighlightTeX is syntax highlight for TeX.
SyntaxHighlightTeX SyntaxHighlight = "tex"
// SyntaxHighlightDiff is syntax highlight for Diff.
SyntaxHighlightDiff SyntaxHighlight = "diff"
// SyntaxHighlightApache is syntax highlight for Apache.
SyntaxHighlightApache SyntaxHighlight = "apache"
// SyntaxHighlightDockerfile is syntax highlight for Dockerfile.
SyntaxHighlightDockerfile SyntaxHighlight = "dockerfile"
// SyntaxHighlightMermaid is syntax highlight for Mermaid.
SyntaxHighlightMermaid SyntaxHighlight = "mermaid"
)
// TableOfContentsDepth represents the depth level for table of contents.
type TableOfContentsDepth int
const (
// TableOfContentsDepthH1 includes only H1 headers in the table of contents.
TableOfContentsDepthH1 TableOfContentsDepth = 1
// TableOfContentsDepthH2 includes H1 and H2 headers in the table of contents.
TableOfContentsDepthH2 TableOfContentsDepth = 2
// TableOfContentsDepthH3 includes H1, H2, and H3 headers in the table of contents.
TableOfContentsDepthH3 TableOfContentsDepth = 3
// TableOfContentsDepthH4 includes H1, H2, H3, and H4 headers in the table of contents.
TableOfContentsDepthH4 TableOfContentsDepth = 4
// TableOfContentsDepthH5 includes H1, H2, H3, H4, and H5 headers in the table of contents.
TableOfContentsDepthH5 TableOfContentsDepth = 5
// TableOfContentsDepthH6 includes all headers (H1 through H6) in the table of contents.
TableOfContentsDepthH6 TableOfContentsDepth = 6
)
const (
// TableOfContentsMarkerBegin is the marker for the beginning of the table of contents.
TableOfContentsMarkerBegin = "<!-- BEGIN_TOC -->"
// TableOfContentsMarkerEnd is the marker for the end of the table of contents.
TableOfContentsMarkerEnd = "<!-- END_TOC -->"
)
// TableOfContentsOptions contains options for generating the table of contents.
type TableOfContentsOptions struct {
// MinDepth is the minimum header level to include (e.g., 2 for H2 and deeper).
MinDepth TableOfContentsDepth
// MaxDepth is the maximum header level to include (e.g., 4 for H4 and shallower).
MaxDepth TableOfContentsDepth
}
// headerInfo stores information about a header for table of contents generation.
type headerInfo struct {
level TableOfContentsDepth
text string
}
// Markdown is markdown text.
type Markdown struct {
// body is markdown body.
body []string
// dest is output destination for markdown body.
dest io.Writer
// err manages errors that occur in all parts of the markdown building.
err error
// headers stores header information for table of contents generation.
headers []headerInfo
// tocOptions stores the table of contents generation options.
tocOptions *TableOfContentsOptions
// tocInserted indicates whether a table of contents placeholder has been generated.
tocInserted bool
}
// NewMarkdown returns new Markdown.
func NewMarkdown(w io.Writer) *Markdown {
return &Markdown{
body: []string{},
dest: w,
headers: []headerInfo{},
}
}
// String returns markdown text.
func (m *Markdown) String() string {
content := strings.Join(m.body, internal.LineFeed())
// Replace table of contents placeholders with actual table of contents content if present
if m.tocInserted && m.tocOptions != nil {
tocContent := m.generateTableOfContents()
if len(tocContent) > 0 {
tocText := strings.Join(tocContent, internal.LineFeed())
placeholder := TableOfContentsMarkerBegin + internal.LineFeed() + TableOfContentsMarkerEnd
replacement := TableOfContentsMarkerBegin + internal.LineFeed() + tocText + internal.LineFeed() + TableOfContentsMarkerEnd
content = strings.ReplaceAll(content, placeholder, replacement)
}
}
return content
}
// Error returns error.
func (m *Markdown) Error() error {
return m.err
}
// PlainText set plain text
func (m *Markdown) PlainText(text string) *Markdown {
m.body = append(m.body, text)
return m
}
// PlainTextf set plain text with format
func (m *Markdown) PlainTextf(format string, args ...interface{}) *Markdown {
return m.PlainText(fmt.Sprintf(format, args...))
}
// Build writes markdown text to output destination.
func (m *Markdown) Build() error {
if m.dest == nil {
if m.err != nil {
return fmt.Errorf("failed to write markdown text: destination writer is nil: %s", m.err.Error()) //nolint:wrapcheck
}
return errors.New("failed to write markdown text: destination writer is nil")
}
if _, err := fmt.Fprint(m.dest, m.String()); err != nil {
if m.err != nil {
return fmt.Errorf("failed to write markdown text: %w: %s", err, m.err.Error()) //nolint:wrapcheck
}
return fmt.Errorf("failed to write markdown text: %w", err)
}
return m.err
}
// H1 is markdown header.
// If you set text "Hello", it will be converted to "# Hello".
func (m *Markdown) H1(text string) *Markdown {
m.headers = append(m.headers, headerInfo{level: TableOfContentsDepthH1, text: text})
m.body = append(m.body, fmt.Sprintf("# %s", text))
return m
}
// H1f is markdown header with format.
// If you set format "%s", text "Hello", it will be converted to "# Hello".
func (m *Markdown) H1f(format string, args ...interface{}) *Markdown {
return m.H1(fmt.Sprintf(format, args...))
}
// H2 is markdown header.
// If you set text "Hello", it will be converted to "## Hello".
func (m *Markdown) H2(text string) *Markdown {
m.headers = append(m.headers, headerInfo{level: TableOfContentsDepthH2, text: text})
m.body = append(m.body, fmt.Sprintf("## %s", text))
return m
}
// H2f is markdown header with format.
// If you set format "%s", text "Hello", it will be converted to "## Hello".
func (m *Markdown) H2f(format string, args ...interface{}) *Markdown {
return m.H2(fmt.Sprintf(format, args...))
}
// H3 is markdown header.
// If you set text "Hello", it will be converted to "### Hello".
func (m *Markdown) H3(text string) *Markdown {
m.headers = append(m.headers, headerInfo{level: TableOfContentsDepthH3, text: text})
m.body = append(m.body, fmt.Sprintf("### %s", text))
return m
}
// H3f is markdown header with format.
// If you set format "%s", text "Hello", it will be converted to "### Hello".
func (m *Markdown) H3f(format string, args ...interface{}) *Markdown {
return m.H3(fmt.Sprintf(format, args...))
}
// H4 is markdown header.
// If you set text "Hello", it will be converted to "#### Hello".
func (m *Markdown) H4(text string) *Markdown {
m.headers = append(m.headers, headerInfo{level: TableOfContentsDepthH4, text: text})
m.body = append(m.body, fmt.Sprintf("#### %s", text))
return m
}
// H4f is markdown header with format.
// If you set format "%s", text "Hello", it will be converted to "#### Hello".
func (m *Markdown) H4f(format string, args ...interface{}) *Markdown {
return m.H4(fmt.Sprintf(format, args...))
}
// H5 is markdown header.
// If you set text "Hello", it will be converted to "##### Hello".
func (m *Markdown) H5(text string) *Markdown {
m.headers = append(m.headers, headerInfo{level: TableOfContentsDepthH5, text: text})
m.body = append(m.body, fmt.Sprintf("##### %s", text))
return m
}
// H5f is markdown header with format.
// If you set format "%s", text "Hello", it will be converted to "##### Hello".
func (m *Markdown) H5f(format string, args ...interface{}) *Markdown {
return m.H5(fmt.Sprintf(format, args...))
}
// H6 is markdown header.
// If you set text "Hello", it will be converted to "###### Hello".
func (m *Markdown) H6(text string) *Markdown {
m.headers = append(m.headers, headerInfo{level: TableOfContentsDepthH6, text: text})
m.body = append(m.body, fmt.Sprintf("###### %s", text))
return m
}
// H6f is markdown header with format.
// If you set format "%s", text "Hello", it will be converted to "###### Hello".
func (m *Markdown) H6f(format string, args ...interface{}) *Markdown {
return m.H6(fmt.Sprintf(format, args...))
}
// TableOfContents generates a table of contents placeholder that will be replaced when Build() is called.
// The table of contents will include all headers from H1 to the specified maxDepth.
// Only one table of contents can be generated per document.
//
// Example:
//
// markdown.NewMarkdown(os.Stdout).
// H1("Title").
// TableOfContents(markdown.TableOfContentsDepthH3). // Table of contents will be placed here
// H2("Section 1").
// H3("Subsection 1.1").
// Build()
func (m *Markdown) TableOfContents(maxDepth TableOfContentsDepth) *Markdown {
return m.TableOfContentsWithRange(TableOfContentsDepthH1, maxDepth)
}
// TableOfContentsWithRange generates a table of contents placeholder with custom depth range.
// The table of contents will include headers from minDepth to maxDepth inclusive.
// Only one table of contents can be generated per document.
//
// Example:
//
// markdown.NewMarkdown(os.Stdout).
// H1("Title"). // This H1 will not appear in table of contents
// H2("Table of Contents").
// TableOfContentsWithRange(markdown.TableOfContentsDepthH2, markdown.TableOfContentsDepthH4). // Only include H2-H4 in table of contents
// H2("Section 1").
// H3("Subsection 1.1").
// H4("Detail").
// H5("Deep Detail"). // This H5 will not appear in table of contents
// Build()
func (m *Markdown) TableOfContentsWithRange(minDepth, maxDepth TableOfContentsDepth) *Markdown {
if m.tocInserted {
if m.err == nil {
m.err = errors.New("table of contents has already been generated")
}
return m
}
if minDepth < TableOfContentsDepthH1 || minDepth > TableOfContentsDepthH6 {
if m.err == nil {
m.err = fmt.Errorf("invalid minDepth: %d (must be between 1 and 6)", minDepth)
}
return m
}
if maxDepth < TableOfContentsDepthH1 || maxDepth > TableOfContentsDepthH6 {
if m.err == nil {
m.err = fmt.Errorf("invalid maxDepth: %d (must be between 1 and 6)", maxDepth)
}
return m
}
if minDepth > maxDepth {
if m.err == nil {
m.err = fmt.Errorf("minDepth (%d) cannot be greater than maxDepth (%d)", minDepth, maxDepth)
}
return m
}
m.tocOptions = &TableOfContentsOptions{
MinDepth: minDepth,
MaxDepth: maxDepth,
}
m.tocInserted = true
// Insert table of contents placeholder markers
m.body = append(m.body, TableOfContentsMarkerBegin)
m.body = append(m.body, TableOfContentsMarkerEnd)
m.body = append(m.body, "")
return m
}
// generateTableOfContents generates the table of contents based on collected headers and options.
func (m *Markdown) generateTableOfContents() []string {
if m.tocOptions == nil || len(m.headers) == 0 {
return []string{}
}
tocLines := make([]string, 0, len(m.headers))
minIndent := int(m.tocOptions.MinDepth)
anchorCounts := make(map[string]int, len(m.headers))
for _, header := range m.headers {
// Skip headers outside the specified range
if header.level < m.tocOptions.MinDepth || header.level > m.tocOptions.MaxDepth {
continue
}
// Calculate relative indentation
indent := strings.Repeat(" ", int(header.level)-minIndent)
// Generate anchor following GitHub's convention
baseAnchor := generateGitHubAnchor(header.text)
count := anchorCounts[baseAnchor]
anchor := baseAnchor
if count > 0 {
anchor = fmt.Sprintf("%s-%d", baseAnchor, count)
}
anchorCounts[baseAnchor] = count + 1
tocLines = append(tocLines, fmt.Sprintf("%s- [%s](#%s)", indent, header.text, anchor))
}
return tocLines
}
func generateGitHubAnchor(text string) string {
text = strings.ToLower(text)
var b strings.Builder
b.Grow(len(text))
for _, r := range text {
switch {
case r == ' ' || r == '-':
b.WriteRune('-')
case unicode.IsLetter(r) || unicode.IsNumber(r):
b.WriteRune(r)
}
}
return b.String()
}
// Details is markdown details.
func (m *Markdown) Details(summary, text string) *Markdown {
m.body = append(
m.body,
fmt.Sprintf("<details><summary>%s</summary>%s%s%s</details>",
summary, internal.LineFeed(), text, internal.LineFeed()))
return m
}
// Detailsf is markdown details with format.
func (m *Markdown) Detailsf(summary, format string, args ...interface{}) *Markdown {
return m.Details(summary, fmt.Sprintf(format, args...))
}
// BulletList is markdown bullet list.
// If you set text "Hello", it will be converted to "- Hello".
func (m *Markdown) BulletList(text ...string) *Markdown {
for _, v := range text {
m.body = append(m.body, fmt.Sprintf("- %s", v))
}
return m
}
// OrderedList is markdown number list.
// If you set text "Hello", it will be converted to "1. Hello".
func (m *Markdown) OrderedList(text ...string) *Markdown {
for i, v := range text {
m.body = append(m.body, fmt.Sprintf("%d. %s", i+1, v))
}
return m
}
// CheckBoxSet is markdown checkbox list.
type CheckBoxSet struct {
// Checked is whether checked or not.
Checked bool
// Text is checkbox text.
Text string
}
// CheckBox is markdown CheckBox.
func (m *Markdown) CheckBox(set []CheckBoxSet) *Markdown {
for _, v := range set {
if v.Checked {
m.body = append(m.body, fmt.Sprintf("- [x] %s", v.Text))
} else {
m.body = append(m.body, fmt.Sprintf("- [ ] %s", v.Text))
}
}
return m
}
// Blockquote is markdown blockquote.
// If you set text "Hello", it will be converted to "> Hello".
func (m *Markdown) Blockquote(text string) *Markdown {
lines := strings.Split(text, internal.LineFeed())
for _, line := range lines {
m.body = append(m.body, fmt.Sprintf("> %s", line))
}
return m
}
// CodeBlocks is code blocks.
// If you set text "Hello" and lang "go", it will be converted to
// "```go
// Hello
// ```".
func (m *Markdown) CodeBlocks(lang SyntaxHighlight, text string) *Markdown {
m.body = append(m.body,
fmt.Sprintf("```%s%s%s%s```", lang, internal.LineFeed(), text, internal.LineFeed()))
return m
}
// HorizontalRule is markdown horizontal rule.
// It will be converted to "---".
func (m *Markdown) HorizontalRule() *Markdown {
m.body = append(m.body, "---")
return m
}
// TableAlignment represents column alignment in markdown tables.
type TableAlignment int
const (
// AlignDefault represents no specific alignment (left by default).
AlignDefault TableAlignment = iota
// AlignLeft represents left alignment (:------).
AlignLeft
// AlignCenter represents center alignment (:-----:).
AlignCenter
// AlignRight represents right alignment (------:).
AlignRight
)
// TableSet is markdown table.
type TableSet struct {
// Header is table header.
Header []string
// Rows is table record.
Rows [][]string
// Alignment is column alignment for each column.
// If nil or shorter than header length, remaining columns use AlignDefault.
Alignment []TableAlignment
}
// ValidateColumns checks if the number of columns in the header and records match.
func (t *TableSet) ValidateColumns() error {
headerColumns := len(t.Header)
for _, record := range t.Rows {
if len(record) != headerColumns {
return ErrMismatchColumn
}
}
return nil
}
// Table is markdown table with alignment support.
func (m *Markdown) Table(t TableSet) *Markdown {
if err := t.ValidateColumns(); err != nil {
if m.err != nil {
m.err = fmt.Errorf("failed to validate columns: %w: %s", err, m.err.Error()) //nolint:wrapcheck
} else {
m.err = fmt.Errorf("failed to validate columns: %w", err)
}
return m
}
if len(t.Header) == 0 {
return m
}
var buf strings.Builder
// Write header row
buf.WriteString("|")
for _, header := range t.Header {
buf.WriteString(" ")
buf.WriteString(header)
buf.WriteString(" |")
}
buf.WriteString(internal.LineFeed())
// Write separator row with alignment
buf.WriteString("|")
for i := 0; i < len(t.Header); i++ {
align := AlignDefault
if i < len(t.Alignment) {
align = t.Alignment[i]
}
switch align {
case AlignDefault:
buf.WriteString("---------|")
case AlignLeft:
buf.WriteString(":--------|")
case AlignCenter:
buf.WriteString(":-------:|")
case AlignRight:
buf.WriteString("--------:|")
}
}
buf.WriteString(internal.LineFeed())
// Write data rows
for _, row := range t.Rows {
buf.WriteString("|")
for _, cell := range row {
buf.WriteString(" ")
buf.WriteString(cell)
buf.WriteString(" |")
}
buf.WriteString(internal.LineFeed())
}
m.body = append(m.body, buf.String())
return m
}
// TableOptions is markdown table options.
type TableOptions struct {
// AutoWrapText is whether to wrap the text automatically.
AutoWrapText bool
// AutoFormatHeaders is whether to format the header automatically.
AutoFormatHeaders bool
}
// CustomTable is markdown table. This is so not break the original Table function. with Possible breaking changes.
func (m *Markdown) CustomTable(t TableSet, options TableOptions) *Markdown {
if err := t.ValidateColumns(); err != nil {
// NOTE: If go version is 1.20, use errors.Join
if m.err != nil {
m.err = fmt.Errorf("failed to validate columns: %w: %s", err, m.err.Error()) //nolint:wrapcheck
} else {
m.err = fmt.Errorf("failed to validate columns: %w", err)
}
}
buf := &strings.Builder{}
table := tablewriter.NewTable(
buf,
tablewriter.WithRenderer(
renderer.NewBlueprint(
tw.Rendition{
Symbols: tw.NewSymbolCustom("Markdown").
WithHeaderLeft("|").
WithHeaderRight("|").
WithColumn("|").
WithMidLeft("|").
WithMidRight("|").
WithCenter("|"),
Borders: tw.Border{
Left: tw.On,
Top: tw.Off,
Right: tw.On,
Bottom: tw.Off,
},
},
),
),
tablewriter.WithConfig(tablewriter.Config{
Header: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoFormat: func() tw.State {
if options.AutoFormatHeaders {
return tw.Success
}
return tw.Fail
}(),
},
},
Row: tw.CellConfig{
Formatting: tw.CellFormatting{
AutoWrap: func() int {
if options.AutoWrapText {
return tw.WrapNormal
}
return tw.WrapNone
}(),
AutoFormat: func() tw.State {
if options.AutoFormatHeaders {
return tw.Success
}
return tw.Fail
}(),
},
Alignment: tw.CellAlignment{Global: tw.AlignNone},
},
}),
)
table.Header(t.Header)
if err := table.Bulk(t.Rows); err != nil {
m.err = errors.Join(m.err, fmt.Errorf("failed to add rows to table: %w", err))
return m
}
// This is so if the user wants to change the table settings they can
if err := table.Render(); err != nil {
m.err = errors.Join(m.err, fmt.Errorf("failed to render table: %w", err))
return m
}
m.body = append(m.body, buf.String())
return m
}
// LF is line feed.
func (m *Markdown) LF() *Markdown {
m.body = append(m.body, " ")
return m
}