github.com/xushiwei/go@v0.0.0-20130601165731-2b9d83f45bc9/src/cmd/godoc/codewalk.go (about)

     1  // Copyright 2010 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // The /doc/codewalk/ tree is synthesized from codewalk descriptions,
     6  // files named $GOROOT/doc/codewalk/*.xml.
     7  // For an example and a description of the format, see
     8  // http://golang.org/doc/codewalk/codewalk or run godoc -http=:6060
     9  // and see http://localhost:6060/doc/codewalk/codewalk .
    10  // That page is itself a codewalk; the source code for it is
    11  // $GOROOT/doc/codewalk/codewalk.xml.
    12  
    13  package main
    14  
    15  import (
    16  	"encoding/xml"
    17  	"errors"
    18  	"fmt"
    19  	"io"
    20  	"log"
    21  	"net/http"
    22  	"os"
    23  	"regexp"
    24  	"sort"
    25  	"strconv"
    26  	"strings"
    27  	"text/template"
    28  	"unicode/utf8"
    29  )
    30  
    31  // Handler for /doc/codewalk/ and below.
    32  func codewalk(w http.ResponseWriter, r *http.Request) {
    33  	relpath := r.URL.Path[len("/doc/codewalk/"):]
    34  	abspath := r.URL.Path
    35  
    36  	r.ParseForm()
    37  	if f := r.FormValue("fileprint"); f != "" {
    38  		codewalkFileprint(w, r, f)
    39  		return
    40  	}
    41  
    42  	// If directory exists, serve list of code walks.
    43  	dir, err := fs.Lstat(abspath)
    44  	if err == nil && dir.IsDir() {
    45  		codewalkDir(w, r, relpath, abspath)
    46  		return
    47  	}
    48  
    49  	// If file exists, serve using standard file server.
    50  	if err == nil {
    51  		serveFile(w, r)
    52  		return
    53  	}
    54  
    55  	// Otherwise append .xml and hope to find
    56  	// a codewalk description, but before trim
    57  	// the trailing /.
    58  	abspath = strings.TrimRight(abspath, "/")
    59  	cw, err := loadCodewalk(abspath + ".xml")
    60  	if err != nil {
    61  		log.Print(err)
    62  		serveError(w, r, relpath, err)
    63  		return
    64  	}
    65  
    66  	// Canonicalize the path and redirect if changed
    67  	if redirect(w, r) {
    68  		return
    69  	}
    70  
    71  	servePage(w, Page{
    72  		Title:    "Codewalk: " + cw.Title,
    73  		Tabtitle: cw.Title,
    74  		Body:     applyTemplate(codewalkHTML, "codewalk", cw),
    75  	})
    76  }
    77  
    78  // A Codewalk represents a single codewalk read from an XML file.
    79  type Codewalk struct {
    80  	Title string      `xml:"title,attr"`
    81  	File  []string    `xml:"file"`
    82  	Step  []*Codestep `xml:"step"`
    83  }
    84  
    85  // A Codestep is a single step in a codewalk.
    86  type Codestep struct {
    87  	// Filled in from XML
    88  	Src   string `xml:"src,attr"`
    89  	Title string `xml:"title,attr"`
    90  	XML   string `xml:",innerxml"`
    91  
    92  	// Derived from Src; not in XML.
    93  	Err    error
    94  	File   string
    95  	Lo     int
    96  	LoByte int
    97  	Hi     int
    98  	HiByte int
    99  	Data   []byte
   100  }
   101  
   102  // String method for printing in template.
   103  // Formats file address nicely.
   104  func (st *Codestep) String() string {
   105  	s := st.File
   106  	if st.Lo != 0 || st.Hi != 0 {
   107  		s += fmt.Sprintf(":%d", st.Lo)
   108  		if st.Lo != st.Hi {
   109  			s += fmt.Sprintf(",%d", st.Hi)
   110  		}
   111  	}
   112  	return s
   113  }
   114  
   115  // loadCodewalk reads a codewalk from the named XML file.
   116  func loadCodewalk(filename string) (*Codewalk, error) {
   117  	f, err := fs.Open(filename)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	defer f.Close()
   122  	cw := new(Codewalk)
   123  	d := xml.NewDecoder(f)
   124  	d.Entity = xml.HTMLEntity
   125  	err = d.Decode(cw)
   126  	if err != nil {
   127  		return nil, &os.PathError{Op: "parsing", Path: filename, Err: err}
   128  	}
   129  
   130  	// Compute file list, evaluate line numbers for addresses.
   131  	m := make(map[string]bool)
   132  	for _, st := range cw.Step {
   133  		i := strings.Index(st.Src, ":")
   134  		if i < 0 {
   135  			i = len(st.Src)
   136  		}
   137  		filename := st.Src[0:i]
   138  		data, err := ReadFile(fs, filename)
   139  		if err != nil {
   140  			st.Err = err
   141  			continue
   142  		}
   143  		if i < len(st.Src) {
   144  			lo, hi, err := addrToByteRange(st.Src[i+1:], 0, data)
   145  			if err != nil {
   146  				st.Err = err
   147  				continue
   148  			}
   149  			// Expand match to line boundaries.
   150  			for lo > 0 && data[lo-1] != '\n' {
   151  				lo--
   152  			}
   153  			for hi < len(data) && (hi == 0 || data[hi-1] != '\n') {
   154  				hi++
   155  			}
   156  			st.Lo = byteToLine(data, lo)
   157  			st.Hi = byteToLine(data, hi-1)
   158  		}
   159  		st.Data = data
   160  		st.File = filename
   161  		m[filename] = true
   162  	}
   163  
   164  	// Make list of files
   165  	cw.File = make([]string, len(m))
   166  	i := 0
   167  	for f := range m {
   168  		cw.File[i] = f
   169  		i++
   170  	}
   171  	sort.Strings(cw.File)
   172  
   173  	return cw, nil
   174  }
   175  
   176  // codewalkDir serves the codewalk directory listing.
   177  // It scans the directory for subdirectories or files named *.xml
   178  // and prepares a table.
   179  func codewalkDir(w http.ResponseWriter, r *http.Request, relpath, abspath string) {
   180  	type elem struct {
   181  		Name  string
   182  		Title string
   183  	}
   184  
   185  	dir, err := fs.ReadDir(abspath)
   186  	if err != nil {
   187  		log.Print(err)
   188  		serveError(w, r, relpath, err)
   189  		return
   190  	}
   191  	var v []interface{}
   192  	for _, fi := range dir {
   193  		name := fi.Name()
   194  		if fi.IsDir() {
   195  			v = append(v, &elem{name + "/", ""})
   196  		} else if strings.HasSuffix(name, ".xml") {
   197  			cw, err := loadCodewalk(abspath + "/" + name)
   198  			if err != nil {
   199  				continue
   200  			}
   201  			v = append(v, &elem{name[0 : len(name)-len(".xml")], cw.Title})
   202  		}
   203  	}
   204  
   205  	servePage(w, Page{
   206  		Title: "Codewalks",
   207  		Body:  applyTemplate(codewalkdirHTML, "codewalkdir", v),
   208  	})
   209  }
   210  
   211  // codewalkFileprint serves requests with ?fileprint=f&lo=lo&hi=hi.
   212  // The filename f has already been retrieved and is passed as an argument.
   213  // Lo and hi are the numbers of the first and last line to highlight
   214  // in the response.  This format is used for the middle window pane
   215  // of the codewalk pages.  It is a separate iframe and does not get
   216  // the usual godoc HTML wrapper.
   217  func codewalkFileprint(w http.ResponseWriter, r *http.Request, f string) {
   218  	abspath := f
   219  	data, err := ReadFile(fs, abspath)
   220  	if err != nil {
   221  		log.Print(err)
   222  		serveError(w, r, f, err)
   223  		return
   224  	}
   225  	lo, _ := strconv.Atoi(r.FormValue("lo"))
   226  	hi, _ := strconv.Atoi(r.FormValue("hi"))
   227  	if hi < lo {
   228  		hi = lo
   229  	}
   230  	lo = lineToByte(data, lo)
   231  	hi = lineToByte(data, hi+1)
   232  
   233  	// Put the mark 4 lines before lo, so that the iframe
   234  	// shows a few lines of context before the highlighted
   235  	// section.
   236  	n := 4
   237  	mark := lo
   238  	for ; mark > 0 && n > 0; mark-- {
   239  		if data[mark-1] == '\n' {
   240  			if n--; n == 0 {
   241  				break
   242  			}
   243  		}
   244  	}
   245  
   246  	io.WriteString(w, `<style type="text/css">@import "/doc/codewalk/codewalk.css";</style><pre>`)
   247  	template.HTMLEscape(w, data[0:mark])
   248  	io.WriteString(w, "<a name='mark'></a>")
   249  	template.HTMLEscape(w, data[mark:lo])
   250  	if lo < hi {
   251  		io.WriteString(w, "<div class='codewalkhighlight'>")
   252  		template.HTMLEscape(w, data[lo:hi])
   253  		io.WriteString(w, "</div>")
   254  	}
   255  	template.HTMLEscape(w, data[hi:])
   256  	io.WriteString(w, "</pre>")
   257  }
   258  
   259  // addrToByte evaluates the given address starting at offset start in data.
   260  // It returns the lo and hi byte offset of the matched region within data.
   261  // See http://plan9.bell-labs.com/sys/doc/sam/sam.html Table II
   262  // for details on the syntax.
   263  func addrToByteRange(addr string, start int, data []byte) (lo, hi int, err error) {
   264  	var (
   265  		dir        byte
   266  		prevc      byte
   267  		charOffset bool
   268  	)
   269  	lo = start
   270  	hi = start
   271  	for addr != "" && err == nil {
   272  		c := addr[0]
   273  		switch c {
   274  		default:
   275  			err = errors.New("invalid address syntax near " + string(c))
   276  		case ',':
   277  			if len(addr) == 1 {
   278  				hi = len(data)
   279  			} else {
   280  				_, hi, err = addrToByteRange(addr[1:], hi, data)
   281  			}
   282  			return
   283  
   284  		case '+', '-':
   285  			if prevc == '+' || prevc == '-' {
   286  				lo, hi, err = addrNumber(data, lo, hi, prevc, 1, charOffset)
   287  			}
   288  			dir = c
   289  
   290  		case '$':
   291  			lo = len(data)
   292  			hi = len(data)
   293  			if len(addr) > 1 {
   294  				dir = '+'
   295  			}
   296  
   297  		case '#':
   298  			charOffset = true
   299  
   300  		case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
   301  			var i int
   302  			for i = 1; i < len(addr); i++ {
   303  				if addr[i] < '0' || addr[i] > '9' {
   304  					break
   305  				}
   306  			}
   307  			var n int
   308  			n, err = strconv.Atoi(addr[0:i])
   309  			if err != nil {
   310  				break
   311  			}
   312  			lo, hi, err = addrNumber(data, lo, hi, dir, n, charOffset)
   313  			dir = 0
   314  			charOffset = false
   315  			prevc = c
   316  			addr = addr[i:]
   317  			continue
   318  
   319  		case '/':
   320  			var i, j int
   321  		Regexp:
   322  			for i = 1; i < len(addr); i++ {
   323  				switch addr[i] {
   324  				case '\\':
   325  					i++
   326  				case '/':
   327  					j = i + 1
   328  					break Regexp
   329  				}
   330  			}
   331  			if j == 0 {
   332  				j = i
   333  			}
   334  			pattern := addr[1:i]
   335  			lo, hi, err = addrRegexp(data, lo, hi, dir, pattern)
   336  			prevc = c
   337  			addr = addr[j:]
   338  			continue
   339  		}
   340  		prevc = c
   341  		addr = addr[1:]
   342  	}
   343  
   344  	if err == nil && dir != 0 {
   345  		lo, hi, err = addrNumber(data, lo, hi, dir, 1, charOffset)
   346  	}
   347  	if err != nil {
   348  		return 0, 0, err
   349  	}
   350  	return lo, hi, nil
   351  }
   352  
   353  // addrNumber applies the given dir, n, and charOffset to the address lo, hi.
   354  // dir is '+' or '-', n is the count, and charOffset is true if the syntax
   355  // used was #n.  Applying +n (or +#n) means to advance n lines
   356  // (or characters) after hi.  Applying -n (or -#n) means to back up n lines
   357  // (or characters) before lo.
   358  // The return value is the new lo, hi.
   359  func addrNumber(data []byte, lo, hi int, dir byte, n int, charOffset bool) (int, int, error) {
   360  	switch dir {
   361  	case 0:
   362  		lo = 0
   363  		hi = 0
   364  		fallthrough
   365  
   366  	case '+':
   367  		if charOffset {
   368  			pos := hi
   369  			for ; n > 0 && pos < len(data); n-- {
   370  				_, size := utf8.DecodeRune(data[pos:])
   371  				pos += size
   372  			}
   373  			if n == 0 {
   374  				return pos, pos, nil
   375  			}
   376  			break
   377  		}
   378  		// find next beginning of line
   379  		if hi > 0 {
   380  			for hi < len(data) && data[hi-1] != '\n' {
   381  				hi++
   382  			}
   383  		}
   384  		lo = hi
   385  		if n == 0 {
   386  			return lo, hi, nil
   387  		}
   388  		for ; hi < len(data); hi++ {
   389  			if data[hi] != '\n' {
   390  				continue
   391  			}
   392  			switch n--; n {
   393  			case 1:
   394  				lo = hi + 1
   395  			case 0:
   396  				return lo, hi + 1, nil
   397  			}
   398  		}
   399  
   400  	case '-':
   401  		if charOffset {
   402  			// Scan backward for bytes that are not UTF-8 continuation bytes.
   403  			pos := lo
   404  			for ; pos > 0 && n > 0; pos-- {
   405  				if data[pos]&0xc0 != 0x80 {
   406  					n--
   407  				}
   408  			}
   409  			if n == 0 {
   410  				return pos, pos, nil
   411  			}
   412  			break
   413  		}
   414  		// find earlier beginning of line
   415  		for lo > 0 && data[lo-1] != '\n' {
   416  			lo--
   417  		}
   418  		hi = lo
   419  		if n == 0 {
   420  			return lo, hi, nil
   421  		}
   422  		for ; lo >= 0; lo-- {
   423  			if lo > 0 && data[lo-1] != '\n' {
   424  				continue
   425  			}
   426  			switch n--; n {
   427  			case 1:
   428  				hi = lo
   429  			case 0:
   430  				return lo, hi, nil
   431  			}
   432  		}
   433  	}
   434  
   435  	return 0, 0, errors.New("address out of range")
   436  }
   437  
   438  // addrRegexp searches for pattern in the given direction starting at lo, hi.
   439  // The direction dir is '+' (search forward from hi) or '-' (search backward from lo).
   440  // Backward searches are unimplemented.
   441  func addrRegexp(data []byte, lo, hi int, dir byte, pattern string) (int, int, error) {
   442  	re, err := regexp.Compile(pattern)
   443  	if err != nil {
   444  		return 0, 0, err
   445  	}
   446  	if dir == '-' {
   447  		// Could implement reverse search using binary search
   448  		// through file, but that seems like overkill.
   449  		return 0, 0, errors.New("reverse search not implemented")
   450  	}
   451  	m := re.FindIndex(data[hi:])
   452  	if len(m) > 0 {
   453  		m[0] += hi
   454  		m[1] += hi
   455  	} else if hi > 0 {
   456  		// No match.  Wrap to beginning of data.
   457  		m = re.FindIndex(data)
   458  	}
   459  	if len(m) == 0 {
   460  		return 0, 0, errors.New("no match for " + pattern)
   461  	}
   462  	return m[0], m[1], nil
   463  }
   464  
   465  // lineToByte returns the byte index of the first byte of line n.
   466  // Line numbers begin at 1.
   467  func lineToByte(data []byte, n int) int {
   468  	if n <= 1 {
   469  		return 0
   470  	}
   471  	n--
   472  	for i, c := range data {
   473  		if c == '\n' {
   474  			if n--; n == 0 {
   475  				return i + 1
   476  			}
   477  		}
   478  	}
   479  	return len(data)
   480  }
   481  
   482  // byteToLine returns the number of the line containing the byte at index i.
   483  func byteToLine(data []byte, i int) int {
   484  	l := 1
   485  	for j, c := range data {
   486  		if j == i {
   487  			return l
   488  		}
   489  		if c == '\n' {
   490  			l++
   491  		}
   492  	}
   493  	return l
   494  }