1
0
Fork 0
telegraf/tools/readme_config_includer/generator.go
Daniel Baumann 4978089aab
Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
2025-05-24 07:26:29 +02:00

250 lines
6.9 KiB
Go

// This is a tool to embed 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"
"fmt"
"io"
"log"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/yuin/goldmark"
"github.com/yuin/goldmark/ast"
"github.com/yuin/goldmark/text"
)
var (
// Finds all comment section parts `<-- @includefile -->`
commentIncludesEx = regexp.MustCompile(`<!--\s+(@.+)+\s+-->`)
// Finds all TOML sections of the form `toml @includefile`
tomlIncludesEx = regexp.MustCompile(`[\s"]+(@.+)+"?`)
// Extracts the `includefile` part
includeMatch = regexp.MustCompile(`(?:@([^\s"]+))+`)
)
type includeBlock struct {
Includes []string
Start int
Stop int
Newlines bool
}
func extractIncludeBlock(txt []byte, includesEx *regexp.Regexp, root string) *includeBlock {
includes := includesEx.FindSubmatch(txt)
if len(includes) != 2 {
return nil
}
block := includeBlock{}
for _, inc := range includeMatch.FindAllSubmatch(includes[1], -1) {
if len(inc) != 2 {
continue
}
include := string(inc[1])
// Make absolute paths relative to the include-root if any
if strings.HasPrefix(include, "/") {
if root == "" {
log.Printf("Ignoring absolute include %q without include root...", include)
continue
}
include = filepath.Join(root, include)
}
include, err := filepath.Abs(include)
if err != nil {
log.Printf("Cannot resolve include %q...", 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, include)
}
return &block
}
func insertInclude(buf *bytes.Buffer, include string) error {
file, err := os.Open(include)
if err != nil {
return fmt.Errorf("opening include %q failed: %w", 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: %w", include, err)
}
return nil
}
func insertIncludes(buf *bytes.Buffer, b *includeBlock) error {
// Insert newlines before and after
if b.Newlines {
buf.WriteByte('\n')
}
// Insert all includes in the order they occurred
for i, include := range b.Includes {
if i > 0 {
// Add a separating newline between included blocks
buf.WriteByte('\n')
}
if err := insertInclude(buf, include); err != nil {
return err
}
}
// Make sure we add a trailing newline
if !bytes.HasSuffix(buf.Bytes(), []byte("\n")) || b.Newlines {
buf.WriteByte('\n')
}
return nil
}
func main() {
// Estimate Telegraf root to be able to handle absolute paths
cwd, err := os.Getwd()
if err != nil {
log.Fatalf("Cannot get working directory: %v", err)
}
cwd, err = filepath.Abs(cwd)
if err != nil {
log.Fatalf("Cannot resolve working directory: %v", err)
}
var includeRoot string
if idx := strings.LastIndex(cwd, filepath.FromSlash("/plugins/")); idx > 0 {
includeRoot = cwd[:idx]
}
// 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 rawnode := root.FirstChild(); rawnode != nil; rawnode = rawnode.NextSibling() {
// Only match TOML code nodes
var txt []byte
var start, stop int
var newlines bool
var re *regexp.Regexp
switch node := rawnode.(type) {
case *ast.FencedCodeBlock:
if string(node.Language(readme)) != "toml" {
// Ignore any other node type or language
continue
}
// Extract the block borders
start = node.Info.Segment.Stop + 1
stop = start
lines := node.Lines()
if lines.Len() > 0 {
stop = lines.At(lines.Len() - 1).Stop
}
txt = node.Info.Value(readme)
re = tomlIncludesEx
case *ast.Heading:
if node.ChildCount() < 2 {
continue
}
child, ok := node.LastChild().(*ast.RawHTML)
if !ok || child.Segments.Len() == 0 {
continue
}
segment := child.Segments.At(0)
if !commentIncludesEx.Match(segment.Value(readme)) {
continue
}
start = segment.Stop + 1
stop = len(readme) // necessary for cases with no more headings
for rawnode = rawnode.NextSibling(); rawnode != nil; rawnode = rawnode.NextSibling() {
if h, ok := rawnode.(*ast.Heading); ok && h.Level <= node.Level {
if rawnode.Lines().Len() > 0 {
stop = rawnode.Lines().At(0).Start - h.Level - 1
} else {
//nolint:staticcheck // need to use this since we aren't sure the type
log.Printf("heading without lines: %s", string(rawnode.Text(readme)))
stop = start // safety measure to prevent removing all text
}
// Make sure we also iterate the present heading
rawnode = h.PreviousSibling()
break
}
}
txt = segment.Value(readme)
re = commentIncludesEx
newlines = true
default:
// Ignore everything else
continue
}
// Extract the includes from the node
block := extractIncludeBlock(txt, re, includeRoot)
if block != nil {
block.Start = start
block.Stop = stop
block.Newlines = newlines
blocksToReplace = append(blocksToReplace, block)
}
// Catch the case of heading-end-search exhausted all nodes
if rawnode == nil {
break
}
}
// 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
output.Write(readme[offset:b.Start])
if !bytes.HasSuffix(output.Bytes(), []byte("\n")) {
output.WriteString("\n")
}
offset = b.Stop
// Insert the include file
if err := insertIncludes(&output, b); err != nil {
log.Fatal(err)
}
}
// Copy the remaining of the original file...
output.Write(readme[offset:])
// Write output with same permission as input
file, err := os.OpenFile(inputFilename, os.O_CREATE|os.O_TRUNC|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.Panicf("Writing output file failed: %v", err)
}
}