github.com/anthonyme00/gomarkdoc@v1.0.0/cmd/gomarkdoc/output.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"fmt"
     7  	"io/ioutil"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"time"
    12  
    13  	"github.com/anthonyme00/gomarkdoc"
    14  	"github.com/anthonyme00/gomarkdoc/lang"
    15  	"github.com/anthonyme00/gomarkdoc/logger"
    16  	"github.com/princjef/termdiff"
    17  	"github.com/sergi/go-diff/diffmatchpatch"
    18  )
    19  
    20  func writeOutput(specs []*PackageSpec, opts commandOptions) error {
    21  	log := logger.New(getLogLevel(opts.verbosity))
    22  
    23  	overrides, err := resolveOverrides(opts)
    24  	if err != nil {
    25  		return err
    26  	}
    27  
    28  	out, err := gomarkdoc.NewRenderer(overrides...)
    29  	if err != nil {
    30  		return err
    31  	}
    32  
    33  	header, err := resolveHeader(opts)
    34  	if err != nil {
    35  		return err
    36  	}
    37  
    38  	footer, err := resolveFooter(opts)
    39  	if err != nil {
    40  		return err
    41  	}
    42  
    43  	filePkgs := make(map[string][]*lang.Package)
    44  
    45  	for _, spec := range specs {
    46  		if spec.pkg == nil {
    47  			continue
    48  		}
    49  
    50  		filePkgs[spec.outputFile] = append(filePkgs[spec.outputFile], spec.pkg)
    51  	}
    52  
    53  	var checkErr error
    54  	for fileName, pkgs := range filePkgs {
    55  		file := lang.NewFile(header, footer, pkgs)
    56  
    57  		text, err := out.File(file)
    58  		if err != nil {
    59  			return err
    60  		}
    61  
    62  		checkErr, err = handleFile(log, fileName, text, opts)
    63  		if err != nil {
    64  			return err
    65  		}
    66  	}
    67  
    68  	if checkErr != nil {
    69  		return checkErr
    70  	}
    71  
    72  	return nil
    73  }
    74  
    75  func handleFile(log logger.Logger, fileName string, text string, opts commandOptions) (error, error) {
    76  	if opts.embed && fileName != "" {
    77  		text = embedContents(log, fileName, text)
    78  	}
    79  
    80  	switch {
    81  	case fileName == "":
    82  		fmt.Fprint(os.Stdout, text)
    83  	case opts.check:
    84  		var b bytes.Buffer
    85  		fmt.Fprint(&b, text)
    86  		if err := checkFile(&b, fileName); err != nil {
    87  			return err, nil
    88  		}
    89  	default:
    90  		if err := writeFile(fileName, text); err != nil {
    91  			return nil, fmt.Errorf("failed to write output file %s: %w", fileName, err)
    92  		}
    93  	}
    94  	return nil, nil
    95  }
    96  
    97  func writeFile(fileName string, text string) error {
    98  	folder := filepath.Dir(fileName)
    99  
   100  	if folder != "" {
   101  		if err := os.MkdirAll(folder, 0755); err != nil {
   102  			return fmt.Errorf("failed to create folder %s: %w", folder, err)
   103  		}
   104  	}
   105  
   106  	if err := ioutil.WriteFile(fileName, []byte(text), 0664); err != nil {
   107  		return fmt.Errorf("failed to write file %s: %w", fileName, err)
   108  	}
   109  
   110  	return nil
   111  }
   112  
   113  func checkFile(b *bytes.Buffer, path string) error {
   114  	checkErr := errors.New("output does not match current files. Did you forget to run gomarkdoc?")
   115  
   116  	fileContents, err := os.ReadFile(path)
   117  	if err == os.ErrNotExist {
   118  		fileContents = []byte{}
   119  	} else if err != nil {
   120  		return fmt.Errorf("failed to open file %s for checking: %w", path, err)
   121  	}
   122  
   123  	differ := diffmatchpatch.New()
   124  	diff := differ.DiffBisect(b.String(), string(fileContents), time.Now().Add(time.Second))
   125  
   126  	// Remove equal diffs
   127  	var filtered = make([]diffmatchpatch.Diff, 0, len(diff))
   128  	for _, d := range diff {
   129  		if d.Type == diffmatchpatch.DiffEqual {
   130  			continue
   131  		}
   132  
   133  		filtered = append(filtered, d)
   134  	}
   135  
   136  	if len(filtered) != 0 {
   137  		diffs := termdiff.DiffsFromDiffMatchPatch(diff)
   138  		fmt.Fprintln(os.Stderr)
   139  		termdiff.Fprint(
   140  			os.Stderr,
   141  			path,
   142  			diffs,
   143  			termdiff.WithBeforeText("(expected)"),
   144  			termdiff.WithAfterText("(actual)"),
   145  		)
   146  		return checkErr
   147  	}
   148  
   149  	return nil
   150  }
   151  
   152  var (
   153  	embedStandaloneRegex = regexp.MustCompile(`(?m:^ *)<!--\s*gomarkdoc:embed\s*-->(?m:\s*?$)`)
   154  	embedStartRegex      = regexp.MustCompile(
   155  		`(?m:^ *)<!--\s*gomarkdoc:embed:start\s*-->(?s:.*?)<!--\s*gomarkdoc:embed:end\s*-->(?m:\s*?$)`,
   156  	)
   157  )
   158  
   159  func embedContents(log logger.Logger, fileName string, text string) string {
   160  	embedText := fmt.Sprintf("<!-- gomarkdoc:embed:start -->\n\n%s\n\n<!-- gomarkdoc:embed:end -->", text)
   161  
   162  	data, err := os.ReadFile(fileName)
   163  	if err != nil {
   164  		log.Debugf("unable to find output file %s for embedding. Creating a new file instead", fileName)
   165  		return embedText
   166  	}
   167  
   168  	var replacements int
   169  	data = embedStandaloneRegex.ReplaceAllFunc(data, func(_ []byte) []byte {
   170  		replacements++
   171  		return []byte(embedText)
   172  	})
   173  
   174  	data = embedStartRegex.ReplaceAllFunc(data, func(_ []byte) []byte {
   175  		replacements++
   176  		return []byte(embedText)
   177  	})
   178  
   179  	if replacements == 0 {
   180  		log.Debugf("no embed markers found. Appending documentation to the end of the file instead")
   181  		return fmt.Sprintf("%s\n\n%s", string(data), text)
   182  	}
   183  
   184  	return string(data)
   185  }