1package main
2
3import (
4	"fmt"
5	"regexp"
6	"sort"
7	"strconv"
8	"strings"
9
10	wordwrap "github.com/mitchellh/go-wordwrap"
11)
12
13type specWriter struct {
14	out strings.Builder
15}
16
17func (w *specWriter) writeConfigBlock(b *configBlock, indent int) {
18	if len(b.entries) == 0 {
19		return
20	}
21
22	for i, entry := range b.entries {
23		// Add a new line to separate from the previous entry
24		if i > 0 {
25			w.out.WriteString("\n")
26		}
27
28		w.writeConfigEntry(entry, indent)
29	}
30}
31
32func (w *specWriter) writeConfigEntry(e *configEntry, indent int) {
33	if e.kind == "block" {
34		// If the block is a root block it will have its dedicated section in the doc,
35		// so here we've just to write down the reference without re-iterating on it.
36		if e.root {
37			// Description
38			w.writeComment(e.blockDesc, indent)
39			if e.block.flagsPrefix != "" {
40				w.writeComment(fmt.Sprintf("The CLI flags prefix for this block config is: %s", e.block.flagsPrefix), indent)
41			}
42
43			// Block reference without entries, because it's a root block
44			w.out.WriteString(pad(indent) + "[" + e.name + ": <" + e.block.name + ">]\n")
45		} else {
46			// Description
47			w.writeComment(e.blockDesc, indent)
48
49			// Name
50			w.out.WriteString(pad(indent) + e.name + ":\n")
51
52			// Entries
53			w.writeConfigBlock(e.block, indent+tabWidth)
54		}
55	}
56
57	if e.kind == "field" {
58		// Description
59		w.writeComment(e.fieldDesc, indent)
60		w.writeFlag(e.fieldFlag, indent)
61
62		// Specification
63		fieldDefault := e.fieldDefault
64		if e.fieldType == "string" {
65			fieldDefault = strconv.Quote(fieldDefault)
66		} else if e.fieldType == "duration" {
67			fieldDefault = cleanupDuration(fieldDefault)
68		}
69
70		if e.required {
71			w.out.WriteString(pad(indent) + e.name + ": <" + e.fieldType + "> | default = " + fieldDefault + "\n")
72		} else {
73			w.out.WriteString(pad(indent) + "[" + e.name + ": <" + e.fieldType + "> | default = " + fieldDefault + "]\n")
74		}
75	}
76}
77
78func (w *specWriter) writeFlag(name string, indent int) {
79	if name == "" {
80		return
81	}
82
83	w.out.WriteString(pad(indent) + "# CLI flag: -" + name + "\n")
84}
85
86func (w *specWriter) writeComment(comment string, indent int) {
87	if comment == "" {
88		return
89	}
90
91	wrapped := strings.TrimSpace(wordwrap.WrapString(comment, uint(maxLineWidth-indent-2)))
92	lines := strings.Split(wrapped, "\n")
93
94	for _, line := range lines {
95		w.out.WriteString(pad(indent) + "# " + line + "\n")
96	}
97}
98
99func (w *specWriter) string() string {
100	return strings.TrimSpace(w.out.String())
101}
102
103type markdownWriter struct {
104	out strings.Builder
105}
106
107func (w *markdownWriter) writeConfigDoc(blocks []*configBlock) {
108	// Deduplicate root blocks.
109	uniqueBlocks := map[string]*configBlock{}
110	for _, block := range blocks {
111		uniqueBlocks[block.name] = block
112	}
113
114	// Generate the markdown, honoring the root blocks order.
115	if topBlock, ok := uniqueBlocks[""]; ok {
116		w.writeConfigBlock(topBlock)
117	}
118
119	for _, rootBlock := range rootBlocks {
120		if block, ok := uniqueBlocks[rootBlock.name]; ok {
121			w.writeConfigBlock(block)
122		}
123	}
124}
125
126func (w *markdownWriter) writeConfigBlock(block *configBlock) {
127	// Title
128	if block.name != "" {
129		w.out.WriteString("### `" + block.name + "`\n")
130		w.out.WriteString("\n")
131	}
132
133	// Description
134	if block.desc != "" {
135		desc := block.desc
136
137		// Wrap the config block name with backticks
138		if block.name != "" {
139			desc = regexp.MustCompile(regexp.QuoteMeta(block.name)).ReplaceAllStringFunc(desc, func(input string) string {
140				return "`" + input + "`"
141			})
142		}
143
144		// List of all prefixes used to reference this config block.
145		if len(block.flagsPrefixes) > 1 {
146			sortedPrefixes := sort.StringSlice(block.flagsPrefixes)
147			sortedPrefixes.Sort()
148
149			desc += " The supported CLI flags `<prefix>` used to reference this config block are:\n\n"
150
151			for _, prefix := range sortedPrefixes {
152				if prefix == "" {
153					desc += "- _no prefix_\n"
154				} else {
155					desc += fmt.Sprintf("- `%s`\n", prefix)
156				}
157			}
158
159			// Unfortunately the markdown compiler used by the website generator has a bug
160			// when there's a list followed by a code block (no matter know many newlines
161			// in between). To workaround it we add a non-breaking space.
162			desc += "\n&nbsp;"
163		}
164
165		w.out.WriteString(desc + "\n")
166		w.out.WriteString("\n")
167	}
168
169	// Config specs
170	spec := &specWriter{}
171	spec.writeConfigBlock(block, 0)
172
173	w.out.WriteString("```yaml\n")
174	w.out.WriteString(spec.string() + "\n")
175	w.out.WriteString("```\n")
176	w.out.WriteString("\n")
177}
178
179func (w *markdownWriter) string() string {
180	return strings.TrimSpace(w.out.String())
181}
182
183func pad(length int) string {
184	return strings.Repeat(" ", length)
185}
186
187func cleanupDuration(value string) string {
188	// This is the list of suffixes to remove from the duration if they're not
189	// the whole duration value.
190	suffixes := []string{"0s", "0m"}
191
192	for _, suffix := range suffixes {
193		re := regexp.MustCompile("(^.+\\D)" + suffix + "$")
194
195		if groups := re.FindStringSubmatch(value); len(groups) == 2 {
196			value = groups[1]
197		}
198	}
199
200	return value
201}
202