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  }