summaryrefslogtreecommitdiff
path: root/doc/htmlgen.go
diff options
context:
space:
mode:
Diffstat (limited to 'doc/htmlgen.go')
-rw-r--r--doc/htmlgen.go167
1 files changed, 151 insertions, 16 deletions
diff --git a/doc/htmlgen.go b/doc/htmlgen.go
index 3a8feb8bc..5318a07dc 100644
--- a/doc/htmlgen.go
+++ b/doc/htmlgen.go
@@ -2,46 +2,80 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-// Process plain text into HTML.
+// 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
+// - 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 (
- lines = make([][]byte, 0, 2000) // probably big enough; grows if not
+ 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{' ', ' ', ' ', ' '}
+ indent = []byte(" ")
sectionMarker = []byte("----\n")
preStart = []byte("<pre>")
preEnd = []byte("</pre>\n")
pp = []byte("<p>\n")
+
+ srcPrefix = []byte("!src")
)
func main() {
+ flag.Parse()
read()
- headings()
- coalesce(preStart, foldPre)
- coalesce(tab, foldTabs)
- paragraphs()
- quotes()
+ 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 {
@@ -56,6 +90,7 @@ func read() {
}
}
+// write puts the result on standard output.
func write() {
b := bufio.NewWriter(os.Stdout)
for _, line := range lines {
@@ -64,8 +99,104 @@ func write() {
b.Flush()
}
-// each time prefix is found on a line, call fold and replace
-// line with return value from fold.
+// 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); {
@@ -82,7 +213,7 @@ func coalesce(prefix []byte, fold func(i int) (n int, line []byte)) {
lines = lines[0:j]
}
-// return the <pre> block as a single slice
+// 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) {
@@ -96,7 +227,7 @@ func foldPre(i int) (n int, line []byte) {
return n, buf.Bytes()
}
-// return the tab-indented block as a single <pre>-bounded slice
+// 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")
@@ -104,7 +235,7 @@ func foldTabs(i int) (n int, line []byte) {
if !bytes.HasPrefix(lines[i], tab) {
break
}
- buf.Write(lines[i])
+ buf.Write(lines[i][1:]) // delete leading tab.
n++
i++
}
@@ -112,6 +243,7 @@ func foldTabs(i int) (n int, line []byte) {
return n, buf.Bytes()
}
+// headings turns sections into HTML sections.
func headings() {
b := bufio.NewWriter(os.Stdout)
for i, l := range lines {
@@ -123,6 +255,7 @@ func headings() {
b.Flush()
}
+// paragraphs turns blank lines into paragraph marks.
func paragraphs() {
for i, l := range lines {
if bytes.Equal(l, newline) {
@@ -131,12 +264,14 @@ func paragraphs() {
}
}
+// 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
@@ -162,7 +297,7 @@ func codeQuotes(l []byte) []byte {
return buf.Bytes()
}
-// drop trailing newline
+// trim drops the trailing newline, if present.
func trim(l []byte) []byte {
n := len(l)
if n > 0 && l[n-1] == '\n' {
@@ -171,7 +306,7 @@ func trim(l []byte) []byte {
return l
}
-// expand tabs to spaces. don't worry about columns.
+// expandTabs expands tabs to spaces. It doesn't worry about columns.
func expandTabs(l []byte) []byte {
return bytes.Replace(l, tab, indent, -1)
}