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 " 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