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&nbsp;"
   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  }