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