| // The weave command is a simple preprocessor for markdown files. |
| // It builds a table of contents and processes %include directives. |
| // |
| // Example usage: |
| // |
| // $ go run internal/cmd/weave go-types.md > README.md |
| // |
| // The weave command copies lines of the input file to standard output, with two |
| // exceptions: |
| // |
| // If a line begins with "%toc", it is replaced with a table of contents |
| // consisting of links to the top two levels of headers ("#" and "##"). |
| // |
| // If a line begins with "%include FILENAME TAG", it is replaced with the lines |
| // of the file between lines containing "!+TAG" and "!-TAG". TAG can be omitted, |
| // in which case the delimiters are simply "!+" and "!-". |
| // |
| // Before the included lines, a line of the form |
| // |
| // // go get PACKAGE |
| // |
| // is output, where PACKAGE is constructed from the module path, the |
| // base name of the current directory, and the directory of FILENAME. |
| // This caption can be supressed by putting "-" as the final word of the %include line. |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "fmt" |
| "log" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strings" |
| ) |
| |
| func main() { |
| log.SetFlags(0) |
| log.SetPrefix("weave: ") |
| if len(os.Args) != 2 { |
| log.Fatal("usage: weave input.md\n") |
| } |
| |
| f, err := os.Open(os.Args[1]) |
| if err != nil { |
| log.Fatal(err) |
| } |
| defer f.Close() |
| |
| wd, err := os.Getwd() |
| if err != nil { |
| log.Fatal(err) |
| } |
| curDir := filepath.Base(wd) |
| |
| fmt.Println("<!-- Autogenerated by weave; DO NOT EDIT -->") |
| |
| // Pass 1: extract table of contents. |
| var toc []string |
| in := bufio.NewScanner(f) |
| for in.Scan() { |
| line := in.Text() |
| if line == "" || (line[0] != '#' && line[0] != '%') { |
| continue |
| } |
| line = strings.TrimSpace(line) |
| if line == "%toc" { |
| toc = nil |
| } else if strings.HasPrefix(line, "# ") || strings.HasPrefix(line, "## ") { |
| words := strings.Fields(line) |
| depth := len(words[0]) |
| words = words[1:] |
| text := strings.Join(words, " ") |
| for i := range words { |
| words[i] = strings.ToLower(words[i]) |
| } |
| line = fmt.Sprintf("%s1. [%s](#%s)", |
| strings.Repeat("\t", depth-1), text, strings.Join(words, "-")) |
| toc = append(toc, line) |
| } |
| } |
| if in.Err() != nil { |
| log.Fatal(in.Err()) |
| } |
| |
| // Pass 2. |
| if _, err := f.Seek(0, os.SEEK_SET); err != nil { |
| log.Fatalf("can't rewind input: %v", err) |
| } |
| in = bufio.NewScanner(f) |
| for in.Scan() { |
| line := in.Text() |
| switch { |
| case strings.HasPrefix(line, "%toc"): // ToC |
| for _, h := range toc { |
| fmt.Println(h) |
| } |
| case strings.HasPrefix(line, "%include"): |
| words := strings.Fields(line) |
| if len(words) < 2 { |
| log.Fatal(line) |
| } |
| filename := words[1] |
| |
| // Show caption unless '-' follows. |
| if len(words) < 4 || words[3] != "-" { |
| fmt.Printf(" // go get golang.org/x/example/%s/%s\n\n", |
| curDir, filepath.Dir(filename)) |
| } |
| |
| section := "" |
| if len(words) > 2 { |
| section = words[2] |
| } |
| s, err := include(filename, section) |
| if err != nil { |
| log.Fatal(err) |
| } |
| fmt.Println("```") |
| fmt.Println(cleanListing(s)) // TODO(adonovan): escape /^```/ in s |
| fmt.Println("```") |
| default: |
| fmt.Println(line) |
| } |
| } |
| if in.Err() != nil { |
| log.Fatal(in.Err()) |
| } |
| } |
| |
| // include processes an included file, and returns the included text. |
| // Only lines between those matching !+tag and !-tag will be returned. |
| // This is true even if tag=="". |
| func include(file, tag string) (string, error) { |
| f, err := os.Open(file) |
| if err != nil { |
| return "", err |
| } |
| defer f.Close() |
| |
| startre, err := regexp.Compile("!\\+" + tag + "$") |
| if err != nil { |
| return "", err |
| } |
| endre, err := regexp.Compile("!\\-" + tag + "$") |
| if err != nil { |
| return "", err |
| } |
| |
| var text bytes.Buffer |
| in := bufio.NewScanner(f) |
| var on bool |
| for in.Scan() { |
| line := in.Text() |
| switch { |
| case startre.MatchString(line): |
| on = true |
| case endre.MatchString(line): |
| on = false |
| case on: |
| text.WriteByte('\t') |
| text.WriteString(line) |
| text.WriteByte('\n') |
| } |
| } |
| if in.Err() != nil { |
| return "", in.Err() |
| } |
| if text.Len() == 0 { |
| return "", fmt.Errorf("no lines of %s matched tag %q", file, tag) |
| } |
| return text.String(), nil |
| } |
| |
| func isBlank(line string) bool { return strings.TrimSpace(line) == "" } |
| |
| func indented(line string) bool { |
| return strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") |
| } |
| |
| // cleanListing removes entirely blank leading and trailing lines from |
| // text, and removes n leading tabs. |
| func cleanListing(text string) string { |
| lines := strings.Split(text, "\n") |
| |
| // remove minimum number of leading tabs from all non-blank lines |
| tabs := 999 |
| for i, line := range lines { |
| if strings.TrimSpace(line) == "" { |
| lines[i] = "" |
| } else { |
| if n := leadingTabs(line); n < tabs { |
| tabs = n |
| } |
| } |
| } |
| for i, line := range lines { |
| if line != "" { |
| line := line[tabs:] |
| lines[i] = line // remove leading tabs |
| } |
| } |
| |
| // remove leading blank lines |
| for len(lines) > 0 && lines[0] == "" { |
| lines = lines[1:] |
| } |
| // remove trailing blank lines |
| for len(lines) > 0 && lines[len(lines)-1] == "" { |
| lines = lines[:len(lines)-1] |
| } |
| return strings.Join(lines, "\n") |
| } |
| |
| func leadingTabs(s string) int { |
| var i int |
| for i = 0; i < len(s); i++ { |
| if s[i] != '\t' { |
| break |
| } |
| } |
| return i |
| } |