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