github.com/elves/elvish@v0.15.0/website/cmd/elvdoc/main.go (about) 1 package main 2 3 import ( 4 "bufio" 5 "flag" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "log" 10 "os" 11 "path/filepath" 12 "sort" 13 "strings" 14 ) 15 16 func main() { 17 run(os.Args[1:], os.Stdin, os.Stdout) 18 } 19 20 func run(args []string, in io.Reader, out io.Writer) { 21 flags := flag.NewFlagSet("", flag.ExitOnError) 22 var ( 23 directory = flags.Bool("dir", false, "read from .go files in directories") 24 filter = flags.Bool("filter", false, "act as a Markdown file filter") 25 ns = flags.String("ns", "", "namespace prefix") 26 ) 27 28 err := flags.Parse(args) 29 if err != nil { 30 log.Fatal(err) 31 } 32 args = flags.Args() 33 34 switch { 35 case *directory: 36 extractDirs(args, *ns, out) 37 case *filter: 38 // NOTE: Ignores arguments. 39 filterMarkdown(in, out) 40 case len(args) > 0: 41 extractFiles(args, *ns, out) 42 default: 43 extract(in, *ns, out) 44 } 45 } 46 47 const markdownLeader = "@elvdoc " 48 49 var emptyReader = &strings.Reader{} 50 51 func filterMarkdown(in io.Reader, out io.Writer) { 52 scanner := bufio.NewScanner(in) 53 for scanner.Scan() { 54 line := scanner.Text() 55 if arg := strings.TrimPrefix(line, markdownLeader); arg != line { 56 args := strings.Fields(arg) 57 run(args, emptyReader, out) 58 } else { 59 fmt.Fprintln(out, line) 60 } 61 } 62 if err := scanner.Err(); err != nil && err != io.EOF { 63 log.Fatal(err) 64 } 65 } 66 67 func extractDirs(dirs []string, ns string, out io.Writer) { 68 var files []string 69 for _, dir := range dirs { 70 files = append(files, goFilesInDirectory(dir)...) 71 } 72 extractFiles(files, ns, out) 73 } 74 75 func extractFiles(files []string, ns string, out io.Writer) { 76 reader, cleanup, err := multiFile(files) 77 if err != nil { 78 log.Fatal(err) 79 } 80 defer cleanup() 81 extract(reader, ns, out) 82 } 83 84 // Returns all .go files in the given directory. 85 func goFilesInDirectory(dir string) []string { 86 files, err := ioutil.ReadDir(dir) 87 if err != nil { 88 log.Fatalf("walk %v: %v", dir, err) 89 } 90 var paths []string 91 for _, file := range files { 92 if filepath.Ext(file.Name()) == ".go" { 93 paths = append(paths, filepath.Join(dir, file.Name())) 94 } 95 } 96 return paths 97 } 98 99 // Makes a reader that concatenates multiple files. 100 func multiFile(names []string) (io.Reader, func(), error) { 101 readers := make([]io.Reader, len(names)) 102 closers := make([]io.Closer, len(names)) 103 for i, name := range names { 104 file, err := os.Open(name) 105 if err != nil { 106 for j := 0; j < i; j++ { 107 closers[j].Close() 108 } 109 return nil, nil, err 110 } 111 readers[i] = file 112 closers[i] = file 113 } 114 return io.MultiReader(readers...), func() { 115 for _, closer := range closers { 116 closer.Close() 117 } 118 }, nil 119 } 120 121 func extract(r io.Reader, ns string, w io.Writer) { 122 bufr := bufio.NewReader(r) 123 124 fnDocs := make(map[string]string) 125 varDocs := make(map[string]string) 126 127 // Reads a block of line comments, i.e. a continuous range of lines that 128 // start with //. Returns the content, with the leading // and any spaces 129 // after it removed. The content always ends with a newline, even if the 130 // last line of the comment is the last line of the file without a trailing 131 // newline. 132 // 133 // Will discard the first line after the comment block. 134 readCommentBlock := func() (string, error) { 135 builder := &strings.Builder{} 136 for { 137 line, err := bufr.ReadString('\n') 138 if err == io.EOF && len(line) > 0 { 139 // We pretend that the file always have a trailing newline even 140 // if it does not exist. The next ReadString will return io.EOF 141 // again with an empty line. 142 line += "\n" 143 err = nil 144 } 145 if !strings.HasPrefix(line, "//") || err != nil { 146 // Discard this line, finalize the builder, and return. 147 return builder.String(), err 148 } 149 // The line already has a trailing newline. 150 builder.WriteString(strings.TrimPrefix(line[len("//"):], " ")) 151 } 152 } 153 154 for { 155 line, err := bufr.ReadString('\n') 156 157 const ( 158 varDocPrefix = "//elvdoc:var " 159 fnDocPrefix = "//elvdoc:fn " 160 ) 161 162 if err == nil { 163 switch { 164 case strings.HasPrefix(line, varDocPrefix): 165 varName := line[len(varDocPrefix) : len(line)-1] 166 varDocs[varName], err = readCommentBlock() 167 case strings.HasPrefix(line, fnDocPrefix): 168 fnName := line[len(fnDocPrefix) : len(line)-1] 169 fnDocs[fnName], err = readCommentBlock() 170 } 171 } 172 173 if err != nil { 174 if err != io.EOF { 175 log.Fatalf("read: %v", err) 176 } 177 break 178 } 179 } 180 181 write := func(heading, prefix string, m map[string]string) { 182 fmt.Fprintf(w, "# %s\n", heading) 183 names := make([]string, 0, len(m)) 184 for k := range m { 185 names = append(names, k) 186 } 187 sort.Slice(names, 188 func(i, j int) bool { 189 return symbolForSort(names[i]) < symbolForSort(names[j]) 190 }) 191 for _, name := range names { 192 fmt.Fprintln(w) 193 fmt.Fprintf(w, "## %s\n", prefix+name) 194 // The body is guaranteed to have a trailing newline, hence Fprint 195 // instead of Fprintln. 196 fmt.Fprint(w, m[name]) 197 } 198 } 199 200 if len(varDocs) > 0 { 201 write("Variables", "$"+ns, varDocs) 202 } 203 if len(fnDocs) > 0 { 204 if len(varDocs) > 0 { 205 fmt.Fprintln(w) 206 fmt.Fprintln(w) 207 } 208 write("Functions", ns, fnDocs) 209 } 210 } 211 212 func symbolForSort(s string) string { 213 // If there is a leading dash, move it to the end. 214 if strings.HasPrefix(s, "-") { 215 return s[1:] + "-" 216 } 217 return s 218 }