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 }