github.com/kata-containers/tests@v0.0.0-20240307153542-772105b56064/cmd/check-markdown/main.go (about)

     1  //
     2  // Copyright (c) 2019 Intel Corporation
     3  //
     4  // SPDX-License-Identifier: Apache-2.0
     5  //
     6  
     7  package main
     8  
     9  import (
    10  	"errors"
    11  	"fmt"
    12  	"os"
    13  	"time"
    14  
    15  	"github.com/sirupsen/logrus"
    16  	"github.com/urfave/cli"
    17  )
    18  
    19  type DataToShow int
    20  
    21  const (
    22  	// Character used (after an optional filename) before a heading ID.
    23  	anchorPrefix = "#"
    24  
    25  	// Character used to signify an "absolute link path" which should
    26  	// expand to the value of the document root.
    27  	absoluteLinkPrefix = "/"
    28  
    29  	showLinks    DataToShow = iota
    30  	showHeadings DataToShow = iota
    31  
    32  	textFormat          = "text"
    33  	tsvFormat           = "tsv"
    34  	defaultOutputFormat = textFormat
    35  	defaultSeparator    = "\t"
    36  )
    37  
    38  var (
    39  	// set by the build
    40  	name    = ""
    41  	version = ""
    42  	commit  = ""
    43  
    44  	strict = false
    45  
    46  	// list entry character to use when generating TOCs
    47  	listPrefix = "*"
    48  
    49  	logger *logrus.Entry
    50  
    51  	errNeedFile = errors.New("need markdown file")
    52  )
    53  
    54  // Black Friday sometimes chokes on markdown (I know!!), so record how many
    55  // extra headings we found.
    56  var extraHeadings int
    57  
    58  // Root directory used to handle "absolute link paths" that start with a slash
    59  // to denote the "top directory", like this:
    60  //
    61  // [Foo](/absolute-link.md)
    62  var docRoot string
    63  
    64  var notes = fmt.Sprintf(`
    65  
    66  NOTES:
    67  
    68  - The document root is used to handle markdown references that begin with %q,
    69    denoting that the path that follows is an "absolute path" from the specified
    70    document root path.
    71  
    72  - The order the document nodes are parsed internally is not known to
    73    this program. This means that if multiple errors exist in the document,
    74    running this tool multiple times will error one *one* of the errors, but not
    75    necessarily the same one as last time.
    76  
    77  LIMITATIONS:
    78  
    79  - The default document root only works if this tool is run from the top-level
    80    of a repository.
    81  
    82  `, absoluteLinkPrefix)
    83  
    84  var formatFlag = cli.StringFlag{
    85  	Name:  "format",
    86  	Usage: "display in specified format ('help' to show all)",
    87  	Value: defaultOutputFormat,
    88  }
    89  
    90  var separatorFlag = cli.StringFlag{
    91  	Name:  "separator",
    92  	Usage: fmt.Sprintf("use the specified separator character (%s format only)", tsvFormat),
    93  	Value: defaultSeparator,
    94  }
    95  
    96  var noHeaderFlag = cli.BoolFlag{
    97  	Name:  "no-header",
    98  	Usage: "disable display of header (if format supports one)",
    99  }
   100  
   101  func init() {
   102  	logger = logrus.WithFields(logrus.Fields{
   103  		"name":    name,
   104  		"source":  "check-markdown",
   105  		"version": version,
   106  		"commit":  commit,
   107  		"pid":     os.Getpid(),
   108  	})
   109  
   110  	logger.Logger.Formatter = &logrus.TextFormatter{
   111  		TimestampFormat: time.RFC3339Nano,
   112  		//DisableColors:   true,
   113  	}
   114  
   115  	// Write to stdout to avoid upsetting CI systems that consider stderr
   116  	// writes as indicating an error.
   117  	logger.Logger.Out = os.Stdout
   118  }
   119  
   120  func handleLogging(c *cli.Context) {
   121  	logLevel := logrus.InfoLevel
   122  
   123  	if c.GlobalBool("debug") {
   124  		logLevel = logrus.DebugLevel
   125  	}
   126  
   127  	logger.Logger.SetLevel(logLevel)
   128  }
   129  
   130  func handleDoc(c *cli.Context, createTOC bool) error {
   131  	handleLogging(c)
   132  
   133  	if c.NArg() == 0 {
   134  		return errNeedFile
   135  	}
   136  
   137  	fileName := c.Args().First()
   138  	if fileName == "" {
   139  		return errNeedFile
   140  	}
   141  
   142  	singleDocOnly := c.GlobalBool("single-doc-only")
   143  
   144  	doc := newDoc(fileName, logger)
   145  	doc.ShowTOC = createTOC
   146  
   147  	if createTOC {
   148  		// Only makes sense to generate a single TOC!
   149  		singleDocOnly = true
   150  	}
   151  
   152  	// Parse the main document first
   153  	err := doc.parse()
   154  	if err != nil {
   155  		return err
   156  	}
   157  
   158  	if singleDocOnly && len(docs) > 1 {
   159  		doc.Logger.Debug("Not checking referenced files at user request")
   160  		return nil
   161  	}
   162  
   163  	// Now handle all other docs that the main doc references.
   164  	// This requires care to avoid recursion.
   165  	for {
   166  		count := len(docs)
   167  		parsed := 0
   168  		for _, doc := range docs {
   169  			if doc.Parsed {
   170  				// Document has already been handled
   171  				parsed++
   172  				continue
   173  			}
   174  
   175  			if err := doc.parse(); err != nil {
   176  				return err
   177  			}
   178  		}
   179  
   180  		if parsed == count {
   181  			break
   182  		}
   183  	}
   184  
   185  	err = handleIntraDocLinks()
   186  	if err != nil {
   187  		return err
   188  	}
   189  
   190  	if !createTOC {
   191  		doc.Logger.Info("Checked file")
   192  		doc.showStats()
   193  	}
   194  
   195  	count := len(docs)
   196  
   197  	if count > 1 {
   198  		// Update to ignore main document
   199  		count--
   200  
   201  		doc.Logger.WithField("reference-document-count", count).Info("Checked referenced files")
   202  
   203  		for _, d := range docs {
   204  			if d.Name == doc.Name {
   205  				// Ignore main document
   206  				continue
   207  			}
   208  
   209  			fmt.Printf("\t%q\n", d.Name)
   210  		}
   211  	}
   212  
   213  	// Highlight blackfriday deficiencies
   214  	if !doc.ShowTOC && extraHeadings > 0 {
   215  		doc.Logger.WithField("extra-heading-count", extraHeadings).Debug("Found extra headings")
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  // commonListHandler is used to handle all list operations.
   222  func commonListHandler(context *cli.Context, what DataToShow) error {
   223  	handleLogging(context)
   224  
   225  	handlers := NewDisplayHandlers(context.String("separator"), context.Bool("no-header"))
   226  
   227  	format := context.String("format")
   228  	if format == "help" {
   229  		availableFormats := handlers.Get()
   230  
   231  		for _, format := range availableFormats {
   232  			fmt.Fprintf(outputFile, "%s\n", format)
   233  		}
   234  
   235  		return nil
   236  	}
   237  
   238  	handler := handlers.find(format)
   239  	if handler == nil {
   240  		return fmt.Errorf("no handler for format %q", format)
   241  	}
   242  
   243  	if context.NArg() == 0 {
   244  		return errNeedFile
   245  	}
   246  
   247  	file := context.Args().Get(0)
   248  
   249  	return show(file, logger, handler, what)
   250  }
   251  
   252  func realMain() error {
   253  	cwd, err := os.Getwd()
   254  	if err != nil {
   255  		return err
   256  	}
   257  
   258  	docRoot = cwd
   259  
   260  	cli.VersionPrinter = func(c *cli.Context) {
   261  		fmt.Fprintln(os.Stdout, c.App.Version)
   262  	}
   263  
   264  	cli.AppHelpTemplate = fmt.Sprintf(`%s%s`, cli.AppHelpTemplate, notes)
   265  
   266  	app := cli.NewApp()
   267  	app.Name = name
   268  	app.Version = fmt.Sprintf("%s %s (commit %v)", name, version, commit)
   269  	app.Description = "Tool to check GitHub-Flavoured Markdown (GFM) format documents"
   270  	app.Usage = app.Description
   271  	app.UsageText = fmt.Sprintf("%s [options] file ...", app.Name)
   272  	app.Flags = []cli.Flag{
   273  		cli.BoolFlag{
   274  			Name:  "debug, d",
   275  			Usage: "display debug information",
   276  		},
   277  		cli.StringFlag{
   278  			Name:  "doc-root, r",
   279  			Usage: "specify document root",
   280  			Value: docRoot,
   281  		},
   282  		cli.BoolFlag{
   283  			Name:  "single-doc-only, o",
   284  			Usage: "only check primary (specified) document",
   285  		},
   286  		cli.BoolFlag{
   287  			Name:  "strict, s",
   288  			Usage: "enable strict mode",
   289  		},
   290  	}
   291  
   292  	app.Commands = []cli.Command{
   293  		{
   294  			Name:        "check",
   295  			Usage:       "perform tests on the specified document",
   296  			Description: "Exit code denotes success",
   297  			Action: func(c *cli.Context) error {
   298  				return handleDoc(c, false)
   299  			},
   300  		},
   301  		{
   302  			Name:  "toc",
   303  			Usage: "display a markdown Table of Contents",
   304  			Action: func(c *cli.Context) error {
   305  				return handleDoc(c, true)
   306  			},
   307  		},
   308  		{
   309  			Name:  "list",
   310  			Usage: "display particular parts of the document",
   311  			Subcommands: []cli.Command{
   312  				{
   313  					Name:  "headings",
   314  					Usage: "display headings",
   315  					Flags: []cli.Flag{
   316  						formatFlag,
   317  						noHeaderFlag,
   318  						separatorFlag,
   319  					},
   320  					Action: func(c *cli.Context) error {
   321  						return commonListHandler(c, showHeadings)
   322  					},
   323  				},
   324  				{
   325  					Name:  "links",
   326  					Usage: "display links",
   327  					Flags: []cli.Flag{
   328  						formatFlag,
   329  						noHeaderFlag,
   330  						separatorFlag,
   331  					},
   332  					Action: func(c *cli.Context) error {
   333  						return commonListHandler(c, showLinks)
   334  					},
   335  				},
   336  			},
   337  		},
   338  	}
   339  
   340  	return app.Run(os.Args)
   341  }
   342  
   343  func main() {
   344  	err := realMain()
   345  	if err != nil {
   346  		logger.Fatalf("%v", err)
   347  	}
   348  }