// 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 become 

marks (except inside

 tags)
//	- "quoted strings" become quoted strings

// Lines beginning !src define pieces of program source to be
// extracted from other files and injected as 
 blocks.
// 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 the 
 block 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 into x. func quotes() { for i, l := range lines { lines[i] = codeQuotes(l) } } // quotes turns "x" in the line into x. 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) }