mirror of
https://github.com/golang/go
synced 2024-11-21 18:44:45 -07:00
9cf37c3723
This allows us to drop some crufty scripting and provides a firmer footing for building better tools for preparing documents with source code inside. Also eliminate line numbers from the examples and text. R=golang-dev, adg CC=golang-dev https://golang.org/cl/4650069
313 lines
7.1 KiB
Go
313 lines
7.1 KiB
Go
// 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 <pre> blocks with the first tab deleted
|
|
// - blank lines become <p> marks (except inside <pre> tags)
|
|
// - "quoted strings" become <code>quoted strings</code>
|
|
|
|
// Lines beginning !src define pieces of program source to be
|
|
// extracted from other files and injected as <pre> 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("<pre>")
|
|
preEnd = []byte("</pre>\n")
|
|
pp = []byte("<p>\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("<pre><!--%s\n-->", 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 <pre> 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 <pre>-bounded slice.
|
|
func foldTabs(i int) (n int, line []byte) {
|
|
buf := new(bytes.Buffer)
|
|
buf.WriteString("<pre>\n")
|
|
for i < len(lines) {
|
|
if !bytes.HasPrefix(lines[i], tab) {
|
|
break
|
|
}
|
|
buf.Write(lines[i][1:]) // delete leading tab.
|
|
n++
|
|
i++
|
|
}
|
|
buf.WriteString("</pre>\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("<h2>" + string(trim(lines[i-1])) + "</h2>\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 <code>x</code>.
|
|
func quotes() {
|
|
for i, l := range lines {
|
|
lines[i] = codeQuotes(l)
|
|
}
|
|
}
|
|
|
|
// quotes turns "x" in the line into <code>x</code>.
|
|
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("</code>")
|
|
} else {
|
|
buf.WriteString("<code>")
|
|
}
|
|
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)
|
|
}
|