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 }