github.com/dmaizel/tests@v0.0.0-20210728163746-cae6a2d9cee8/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 }