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