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