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