// This is a tool to embedd configuration files into the README.md of all plugins // It searches for TOML sections in the plugins' README.md and detects includes specified in the form // ```toml [@includeA.conf[ @includeB[ @...]] // Whatever is in here gets replaced. // ``` // Then it will replace everything in this section by the concatenation of the file `includeA.conf`, `includeB` etc. // content. The tool is not stateful, so it can be run multiple time with a stable result as long // as the included files do not change. package main import ( "bytes" "errors" "fmt" "io" "log" //nolint:revive "os" "regexp" "strings" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/text" ) type includeBlock struct { Includes []string Start int Stop int } func (b *includeBlock) extractBlockBorders(node *ast.FencedCodeBlock) { // The node info starts at the language tag and stops right behind it b.Start = node.Info.Segment.Stop + 1 b.Stop = b.Start // To determine the end of the block, we need to iterate to the last line // and take its stop-offset as the end of the block. lines := node.Lines() for i := 0; i < lines.Len(); i++ { b.Stop = lines.At(i).Stop } } func insertInclude(buf *bytes.Buffer, include string) error { file, err := os.Open(include) if err != nil { return fmt.Errorf("opening include %q failed: %v", include, err) } defer file.Close() // Write the include and make sure we get a newline if _, err := io.Copy(buf, file); err != nil { return fmt.Errorf("inserting include %q failed: %v", include, err) } return nil } func insertIncludes(buf *bytes.Buffer, b includeBlock) error { // Insert all includes in the order they occured for _, include := range b.Includes { if err := insertInclude(buf, include); err != nil { return err } } // Make sure we add a trailing newline if !bytes.HasSuffix(buf.Bytes(), []byte("\n")) { if _, err := buf.Write([]byte("\n")); err != nil { return errors.New("adding newline failed") } } return nil } func main() { // Finds all TOML sections of the form `toml @includefile` and extracts the `includefile` part tomlIncludesEx := regexp.MustCompile(`^toml\s+(@.+)+$`) tomlIncludeMatch := regexp.MustCompile(`(?:@([^\s]+))+`) // Get the file permission of the README for later use inputFilename := "README.md" inputFileInfo, err := os.Lstat(inputFilename) if err != nil { log.Fatalf("Cannot get file permissions: %v", err) } perm := inputFileInfo.Mode().Perm() // Read and parse the README markdown file readme, err := os.ReadFile(inputFilename) if err != nil { log.Fatalf("Reading README failed: %v", err) } parser := goldmark.DefaultParser() root := parser.Parse(text.NewReader(readme)) // Walk the markdown to identify the (TOML) parts to replace blocksToReplace := make([]includeBlock, 0) for node := root.FirstChild(); node != nil; node = node.NextSibling() { // Only match TOML code nodes codeNode, ok := node.(*ast.FencedCodeBlock) if !ok || string(codeNode.Language(readme)) != "toml" { // Ignore any other node type or language continue } // Extract the includes from the node includes := tomlIncludesEx.FindSubmatch(codeNode.Info.Text(readme)) if len(includes) != 2 { continue } block := includeBlock{} for _, inc := range tomlIncludeMatch.FindAllSubmatch(includes[1], -1) { if len(inc) != 2 { continue } include := string(inc[1]) // Safeguards to avoid directory traversals and other bad things if strings.ContainsRune(include, os.PathSeparator) { log.Printf("Ignoring include %q for containing a path...", include) continue } if fi, err := os.Stat(include); err != nil || !fi.Mode().IsRegular() { log.Printf("Ignoring include %q as it cannot be found or is not a regular file...", include) continue } block.Includes = append(block.Includes, string(inc[1])) } // Extract the block boarders block.extractBlockBorders(codeNode) blocksToReplace = append(blocksToReplace, block) } // Replace the content of the TOML blocks with includes var output bytes.Buffer output.Grow(len(readme)) offset := 0 for _, b := range blocksToReplace { // Copy everything up to the beginning of the block we want to replace and make sure we get a newline if _, err := output.Write(readme[offset:b.Start]); err != nil { log.Fatalf("Writing non-replaced content failed: %v", err) } if !bytes.HasSuffix(output.Bytes(), []byte("\n")) { if _, err := output.Write([]byte("\n")); err != nil { log.Fatalf("Writing failed: %v", err) } } offset = b.Stop // Insert the include file if err := insertIncludes(&output, b); err != nil { log.Fatal(err) } } // Copy the remainings of the original file... if _, err := output.Write(readme[offset:]); err != nil { log.Fatalf("Writing remaining content failed: %v", err) } // Write output with same permission as input file, err := os.OpenFile(inputFilename, os.O_CREATE|os.O_WRONLY, perm) if err != nil { log.Fatalf("Opening output file failed: %v", err) } defer file.Close() if _, err := output.WriteTo(file); err != nil { log.Fatalf("Writing output file failed: %v", err) } }