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.

144 lines
3.5 KiB

  1. // Copyright 2009 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. package godoc
  5. import (
  6. "bytes"
  7. "encoding/json"
  8. "log"
  9. pathpkg "path"
  10. "strings"
  11. "time"
  12. "golang.org/x/tools/godoc/vfs"
  13. )
  14. var (
  15. doctype = []byte("<!DOCTYPE ")
  16. jsonStart = []byte("<!--{")
  17. jsonEnd = []byte("}-->")
  18. )
  19. // ----------------------------------------------------------------------------
  20. // Documentation Metadata
  21. // TODO(adg): why are some exported and some aren't? -brad
  22. type Metadata struct {
  23. Title string
  24. Subtitle string
  25. Template bool // execute as template
  26. Path string // canonical path for this page
  27. filePath string // filesystem path relative to goroot
  28. }
  29. func (m *Metadata) FilePath() string { return m.filePath }
  30. // extractMetadata extracts the Metadata from a byte slice.
  31. // It returns the Metadata value and the remaining data.
  32. // If no metadata is present the original byte slice is returned.
  33. //
  34. func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
  35. tail = b
  36. if !bytes.HasPrefix(b, jsonStart) {
  37. return
  38. }
  39. end := bytes.Index(b, jsonEnd)
  40. if end < 0 {
  41. return
  42. }
  43. b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
  44. if err = json.Unmarshal(b, &meta); err != nil {
  45. return
  46. }
  47. tail = tail[end+len(jsonEnd):]
  48. return
  49. }
  50. // UpdateMetadata scans $GOROOT/doc for HTML files, reads their metadata,
  51. // and updates the DocMetadata map.
  52. func (c *Corpus) updateMetadata() {
  53. metadata := make(map[string]*Metadata)
  54. var scan func(string) // scan is recursive
  55. scan = func(dir string) {
  56. fis, err := c.fs.ReadDir(dir)
  57. if err != nil {
  58. log.Println("updateMetadata:", err)
  59. return
  60. }
  61. for _, fi := range fis {
  62. name := pathpkg.Join(dir, fi.Name())
  63. if fi.IsDir() {
  64. scan(name) // recurse
  65. continue
  66. }
  67. if !strings.HasSuffix(name, ".html") {
  68. continue
  69. }
  70. // Extract metadata from the file.
  71. b, err := vfs.ReadFile(c.fs, name)
  72. if err != nil {
  73. log.Printf("updateMetadata %s: %v", name, err)
  74. continue
  75. }
  76. meta, _, err := extractMetadata(b)
  77. if err != nil {
  78. log.Printf("updateMetadata: %s: %v", name, err)
  79. continue
  80. }
  81. // Store relative filesystem path in Metadata.
  82. meta.filePath = name
  83. if meta.Path == "" {
  84. // If no Path, canonical path is actual path.
  85. meta.Path = meta.filePath
  86. }
  87. // Store under both paths.
  88. metadata[meta.Path] = &meta
  89. metadata[meta.filePath] = &meta
  90. }
  91. }
  92. scan("/doc")
  93. c.docMetadata.Set(metadata)
  94. }
  95. // MetadataFor returns the *Metadata for a given relative path or nil if none
  96. // exists.
  97. //
  98. func (c *Corpus) MetadataFor(relpath string) *Metadata {
  99. if m, _ := c.docMetadata.Get(); m != nil {
  100. meta := m.(map[string]*Metadata)
  101. // If metadata for this relpath exists, return it.
  102. if p := meta[relpath]; p != nil {
  103. return p
  104. }
  105. // Try with or without trailing slash.
  106. if strings.HasSuffix(relpath, "/") {
  107. relpath = relpath[:len(relpath)-1]
  108. } else {
  109. relpath = relpath + "/"
  110. }
  111. return meta[relpath]
  112. }
  113. return nil
  114. }
  115. // refreshMetadata sends a signal to update DocMetadata. If a refresh is in
  116. // progress the metadata will be refreshed again afterward.
  117. //
  118. func (c *Corpus) refreshMetadata() {
  119. select {
  120. case c.refreshMetadataSignal <- true:
  121. default:
  122. }
  123. }
  124. // RefreshMetadataLoop runs forever, updating DocMetadata when the underlying
  125. // file system changes. It should be launched in a goroutine.
  126. func (c *Corpus) refreshMetadataLoop() {
  127. for {
  128. <-c.refreshMetadataSignal
  129. c.updateMetadata()
  130. time.Sleep(10 * time.Second) // at most once every 10 seconds
  131. }
  132. }