You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

523 lines
11 KiB

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