github.com/grafana/pyroscope@v1.18.0/tools/doc-generator/writer.go (about) 1 // SPDX-License-Identifier: AGPL-3.0-only 2 // Provenance-includes-location: https://github.com/cortexproject/cortex/blob/master/tools/doc-generator/writer.go 3 // Provenance-includes-license: Apache-2.0 4 // Provenance-includes-copyright: The Cortex Authors. 5 6 package main 7 8 import ( 9 "fmt" 10 "sort" 11 "strconv" 12 "strings" 13 14 "github.com/grafana/regexp" 15 "github.com/mitchellh/go-wordwrap" 16 "gopkg.in/yaml.v3" 17 18 "github.com/grafana/pyroscope/tools/doc-generator/parse" 19 ) 20 21 type specWriter struct { 22 out strings.Builder 23 } 24 25 func (w *specWriter) writeConfigBlock(b *parse.ConfigBlock, indent int) { 26 if len(b.Entries) == 0 { 27 return 28 } 29 30 for i, entry := range b.Entries { 31 // Add a new line to separate from the previous entry 32 if i > 0 { 33 w.out.WriteString("\n") 34 } 35 36 w.writeConfigEntry(entry, indent) 37 } 38 } 39 40 func (w *specWriter) writeConfigEntry(e *parse.ConfigEntry, indent int) { 41 if e.Kind == parse.KindBlock { 42 // If the block is a root block it will have its dedicated section in the doc, 43 // so here we've just to write down the reference without re-iterating on it. 44 if e.Root { 45 // Description 46 w.writeComment(e.BlockDesc, indent, 0) 47 if e.Block.FlagsPrefix != "" { 48 w.writeComment(fmt.Sprintf("The CLI flags prefix for this block configuration is: %s", e.Block.FlagsPrefix), indent, 0) 49 } 50 51 // Block reference without entries, because it's a root block 52 w.out.WriteString(pad(indent) + "[" + e.Name + ": <" + e.Block.Name + ">]\n") 53 } else { 54 // Description 55 w.writeComment(e.BlockDesc, indent, 0) 56 57 // Name 58 w.out.WriteString(pad(indent) + e.Name + ":\n") 59 60 // Entries 61 w.writeConfigBlock(e.Block, indent+tabWidth) 62 } 63 } 64 65 if e.Kind == parse.KindField || e.Kind == parse.KindSlice || e.Kind == parse.KindMap { 66 // Description 67 w.writeComment(e.Description(), indent, 0) 68 w.writeExample(e.FieldExample, indent) 69 w.writeFlag(e.FieldFlag, indent) 70 71 // Specification 72 fieldDefault := e.FieldDefault 73 switch e.FieldType { 74 case "string": 75 fieldDefault = strconv.Quote(fieldDefault) 76 case "duration": 77 fieldDefault = cleanupDuration(fieldDefault) 78 } 79 80 if e.Required { 81 w.out.WriteString(pad(indent) + e.Name + ": <" + e.FieldType + "> | default = " + fieldDefault + "\n") 82 } else { 83 w.out.WriteString(pad(indent) + "[" + e.Name + ": <" + e.FieldType + "> | default = " + fieldDefault + "]\n") 84 } 85 } 86 } 87 88 func (w *specWriter) writeFlag(name string, indent int) { 89 if name == "" { 90 return 91 } 92 93 w.out.WriteString(pad(indent) + "# CLI flag: -" + name + "\n") 94 } 95 96 func (w *specWriter) writeComment(comment string, indent, innerIndent int) { 97 if comment == "" { 98 return 99 } 100 101 wrapped := wordwrap.WrapString(comment, uint(maxLineWidth-indent-innerIndent-2)) 102 w.writeWrappedString(wrapped, indent, innerIndent) 103 } 104 105 func (w *specWriter) writeExample(example *parse.FieldExample, indent int) { 106 if example == nil { 107 return 108 } 109 110 w.writeComment("Example:", indent, 0) 111 if example.Comment != "" { 112 w.writeComment(example.Comment, indent, 2) 113 } 114 115 data, err := yaml.Marshal(example.Yaml) 116 if err != nil { 117 panic(fmt.Errorf("can't render example: %w", err)) 118 } 119 120 w.writeWrappedString(string(data), indent, 2) 121 } 122 123 func (w *specWriter) writeWrappedString(s string, indent, innerIndent int) { 124 lines := strings.Split(strings.TrimSpace(s), "\n") 125 for _, line := range lines { 126 w.out.WriteString(pad(indent) + "# " + pad(innerIndent) + line + "\n") 127 } 128 } 129 130 func (w *specWriter) string() string { 131 return strings.TrimSpace(w.out.String()) 132 } 133 134 type markdownWriter struct { 135 out strings.Builder 136 } 137 138 func (w *markdownWriter) writeConfigDoc(blocks []*parse.ConfigBlock) { 139 // Deduplicate root blocks. 140 uniqueBlocks := map[string]*parse.ConfigBlock{} 141 for _, block := range blocks { 142 uniqueBlocks[block.Name] = block 143 } 144 145 // Generate the markdown, honoring the root blocks order. 146 if topBlock, ok := uniqueBlocks[""]; ok { 147 w.writeConfigBlock(topBlock) 148 } 149 150 for _, rootBlock := range parse.RootBlocks { 151 if block, ok := uniqueBlocks[rootBlock.Name]; ok { 152 // Keep the root block description. 153 blockToWrite := *block 154 blockToWrite.Desc = rootBlock.Desc 155 156 w.writeConfigBlock(&blockToWrite) 157 } 158 } 159 } 160 161 func (w *markdownWriter) writeConfigBlock(block *parse.ConfigBlock) { 162 // Title 163 if block.Name != "" { 164 w.out.WriteString("### " + block.Name + "\n") 165 w.out.WriteString("\n") 166 } 167 168 // Description 169 if block.Desc != "" { 170 desc := block.Desc 171 172 // Wrap first instance of the config block name with backticks 173 if block.Name != "" { 174 var matches int 175 nameRegexp := regexp.MustCompile(regexp.QuoteMeta(block.Name)) 176 desc = nameRegexp.ReplaceAllStringFunc(desc, func(input string) string { 177 if matches == 0 { 178 matches++ 179 return "`" + input + "`" 180 } 181 return input 182 }) 183 } 184 185 // List of all prefixes used to reference this config block. 186 if len(block.FlagsPrefixes) > 1 { 187 sortedPrefixes := sort.StringSlice(block.FlagsPrefixes) 188 sortedPrefixes.Sort() 189 190 desc += " The supported CLI flags `<prefix>` used to reference this configuration block are:\n\n" 191 192 for _, prefix := range sortedPrefixes { 193 if prefix == "" { 194 desc += "- _no prefix_\n" 195 } else { 196 desc += fmt.Sprintf("- `%s`\n", prefix) 197 } 198 } 199 200 // Unfortunately the markdown compiler used by the website generator has a bug 201 // when there's a list followed by a code block (no matter know many newlines 202 // in between). To workaround it we add a non-breaking space. 203 desc += "\n " 204 } 205 206 w.out.WriteString(desc + "\n") 207 w.out.WriteString("\n") 208 } 209 210 // Config specs 211 spec := &specWriter{} 212 spec.writeConfigBlock(block, 0) 213 214 w.out.WriteString("```yaml\n") 215 w.out.WriteString(spec.string() + "\n") 216 w.out.WriteString("```\n") 217 w.out.WriteString("\n") 218 } 219 220 func (w *markdownWriter) string() string { 221 return strings.TrimSpace(w.out.String()) 222 } 223 224 func pad(length int) string { 225 return strings.Repeat(" ", length) 226 } 227 228 func cleanupDuration(value string) string { 229 // This is the list of suffixes to remove from the duration if they're not 230 // the whole duration value. 231 suffixes := []string{"0s", "0m"} 232 233 for _, suffix := range suffixes { 234 re := regexp.MustCompile("(^.+\\D)" + suffix + "$") 235 236 if groups := re.FindStringSubmatch(value); len(groups) == 2 { 237 value = groups[1] 238 } 239 } 240 241 return value 242 }