171 lines
5.1 KiB
Go
171 lines
5.1 KiB
Go
|
|
// 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)
|
||
|
|
}
|
||
|
|
}
|