// Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // If --html is set, process plain text into HTML. // - h2's are made from lines followed by a line "----\n" // - tab-indented blocks become
blocks with the first tab deleted // - blank lines becomemarks (except inside
tags) // - "quoted strings" becomequoted strings
// Lines beginning !src define pieces of program source to be // extracted from other files and injected asblocks. // The syntax is simple: 1, 2, or 3 space-separated arguments: // // Whole file: // !src foo.go // One line (here the signature of main): // !src foo.go /^func.main/ // Block of text, determined by start and end (here the body of main): // !src foo.go /^func.main/ /^}/ // // Patterns can be /regular.expression/, a decimal number, or $ // to signify the end of the file. // TODO: the regular expression cannot contain spaces; does this matter? package main import ( "bufio" "bytes" "flag" "fmt" "io/ioutil" "log" "os" "regexp" "strconv" "strings" "template" ) var ( html = flag.Bool("html", true, "process text into HTML") ) var ( // lines holds the input and is reworked in place during processing. lines = make([][]byte, 0, 20000) empty = []byte("") newline = []byte("\n") tab = []byte("\t") quote = []byte(`"`) indent = []byte(" ") sectionMarker = []byte("----\n") preStart = []byte("") preEnd = []byte("\n") pp = []byte("\n") srcPrefix = []byte("!src") ) func main() { flag.Parse() read() programs() if *html { headings() coalesce(preStart, foldPre) coalesce(tab, foldTabs) paragraphs() quotes() } write() } // read turns standard input into a slice of lines. func read() { b := bufio.NewReader(os.Stdin) for { line, err := b.ReadBytes('\n') if err == os.EOF { break } if err != nil { log.Fatal(err) } lines = append(lines, line) } } // write puts the result on standard output. func write() { b := bufio.NewWriter(os.Stdout) for _, line := range lines { b.Write(expandTabs(line)) } b.Flush() } // programs injects source code from !src invocations. func programs() { nlines := make([][]byte, 0, len(lines)*3/2) for _, line := range lines { if bytes.HasPrefix(line, srcPrefix) { line = trim(line)[len(srcPrefix):] prog := srcCommand(string(line)) if *html { nlines = append(nlines, []byte(fmt.Sprintf("
", line))) } for _, l := range prog { nlines = append(nlines, htmlEscape(l)) } if *html { nlines = append(nlines, preEnd) } } else { nlines = append(nlines, line) } } lines = nlines } // srcCommand processes one !src invocation. func srcCommand(command string) [][]byte { // TODO: quoted args so we can have 'a b'? args := strings.Fields(command) if len(args) == 0 || len(args) > 3 { log.Fatal("bad syntax for src command: %s", command) } file := args[0] lines := bytes.SplitAfter(readFile(file), newline) // File plus zero args: whole file: // !src file.go if len(args) == 1 { return lines } start := match(file, 0, lines, string(args[1])) // File plus one arg: one line: // !src file.go /foo/ if len(args) == 2 { return [][]byte{lines[start]} } // File plus two args: range: // !src file.go /foo/ /^}/ end := match(file, start, lines, string(args[2])) return lines[start : end+1] // +1 to include matched line. } // htmlEscape makes sure input is HTML clean, if necessary. func htmlEscape(input []byte) []byte { if !*html || bytes.IndexAny(input, `&"<>`) < 0 { return input } var b bytes.Buffer template.HTMLEscape(&b, input) return b.Bytes() } // readFile reads and returns a file as part of !src processing. func readFile(name string) []byte { file, err := ioutil.ReadFile(name) if err != nil { log.Fatal(err) } return file } // match identifies the input line that matches the pattern in a !src invocation. // If start>0, match lines starting there rather than at the beginning. func match(file string, start int, lines [][]byte, pattern string) int { // $ matches the end of the file. if pattern == "$" { return len(lines) - 1 } // Number matches the line. if i, err := strconv.Atoi(pattern); err == nil { return i - 1 // Lines are 1-indexed. } // /regexp/ matches the line that matches the regexp. if len(pattern) > 2 && pattern[0] == '/' && pattern[len(pattern)-1] == '/' { re, err := regexp.Compile(pattern[1 : len(pattern)-1]) if err != nil { log.Fatal(err) } for i := start; i < len(lines); i++ { if re.Match(lines[i]) { return i } } log.Fatalf("%s: no match for %s", file, pattern) } log.Fatalf("unrecognized pattern: %s", pattern) return 0 } // coalesce combines lines. Each time prefix is found on a line, // it calls fold and replaces the line with return value from fold. func coalesce(prefix []byte, fold func(i int) (n int, line []byte)) { j := 0 // output line number goes up by one each loop for i := 0; i < len(lines); { if bytes.HasPrefix(lines[i], prefix) { nlines, block := fold(i) lines[j] = block i += nlines } else { lines[j] = lines[i] i++ } j++ } lines = lines[0:j] } // foldPre returns theblock as a single slice. func foldPre(i int) (n int, line []byte) { buf := new(bytes.Buffer) for i < len(lines) { buf.Write(lines[i]) n++ if bytes.Equal(lines[i], preEnd) { break } i++ } return n, buf.Bytes() } // foldTabs returns the tab-indented block as a single-bounded slice. func foldTabs(i int) (n int, line []byte) { buf := new(bytes.Buffer) buf.WriteString("\n") for i < len(lines) { if !bytes.HasPrefix(lines[i], tab) { break } buf.Write(lines[i][1:]) // delete leading tab. n++ i++ } buf.WriteString("\n") return n, buf.Bytes() } // headings turns sections into HTML sections. func headings() { b := bufio.NewWriter(os.Stdout) for i, l := range lines { if i > 0 && bytes.Equal(l, sectionMarker) { lines[i-1] = []byte("" + string(trim(lines[i-1])) + "
\n") lines[i] = empty } } b.Flush() } // paragraphs turns blank lines into paragraph marks. func paragraphs() { for i, l := range lines { if bytes.Equal(l, newline) { lines[i] = pp } } } // quotes turns "x" in the file intox
. func quotes() { for i, l := range lines { lines[i] = codeQuotes(l) } } // quotes turns "x" in the line intox
. func codeQuotes(l []byte) []byte { if bytes.HasPrefix(l, preStart) { return l } n := bytes.Index(l, quote) if n < 0 { return l } buf := new(bytes.Buffer) inQuote := false for _, c := range l { if c == '"' { if inQuote { buf.WriteString("") } else { buf.WriteString("") } inQuote = !inQuote } else { buf.WriteByte(c) } } return buf.Bytes() } // trim drops the trailing newline, if present. func trim(l []byte) []byte { n := len(l) if n > 0 && l[n-1] == '\n' { return l[0 : n-1] } return l } // expandTabs expands tabs to spaces. It doesn't worry about columns. func expandTabs(l []byte) []byte { return bytes.Replace(l, tab, indent, -1) }