github.com/muhammadn/cortex@v1.9.1-0.20220510110439-46bb7000d03d/tools/doc-generator/writer.go (about)

     1  package main
     2  
     3  import (
     4  	"fmt"
     5  	"regexp"
     6  	"sort"
     7  	"strconv"
     8  	"strings"
     9  
    10  	wordwrap "github.com/mitchellh/go-wordwrap"
    11  )
    12  
    13  type specWriter struct {
    14  	out strings.Builder
    15  }
    16  
    17  func (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  
    32  func (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  
    78  func (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  
    86  func (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  
    99  func (w *specWriter) string() string {
   100  	return strings.TrimSpace(w.out.String())
   101  }
   102  
   103  type markdownWriter struct {
   104  	out strings.Builder
   105  }
   106  
   107  func (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  
   126  func (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  
   179  func (w *markdownWriter) string() string {
   180  	return strings.TrimSpace(w.out.String())
   181  }
   182  
   183  func pad(length int) string {
   184  	return strings.Repeat(" ", length)
   185  }
   186  
   187  func 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  }