| // Copyright 2009 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package godoc |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "errors" |
| "log" |
| "os" |
| pathpkg "path" |
| "strings" |
| "time" |
| |
| "golang.org/x/tools/godoc/vfs" |
| ) |
| |
| var ( |
| doctype = []byte("<!DOCTYPE ") |
| jsonStart = []byte("<!--{") |
| jsonEnd = []byte("}-->") |
| ) |
| |
| // ---------------------------------------------------------------------------- |
| // Documentation Metadata |
| |
| type Metadata struct { |
| // These fields can be set in the JSON header at the top of a doc. |
| Title string |
| Subtitle string |
| Template bool // execute as template |
| Path string // canonical path for this page |
| AltPaths []string // redirect these other paths to this page |
| |
| // These are internal to the implementation. |
| filePath string // filesystem path relative to goroot |
| } |
| |
| func (m *Metadata) FilePath() string { return m.filePath } |
| |
| // extractMetadata extracts the Metadata from a byte slice. |
| // It returns the Metadata value and the remaining data. |
| // If no metadata is present the original byte slice is returned. |
| func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) { |
| tail = b |
| if !bytes.HasPrefix(b, jsonStart) { |
| return |
| } |
| end := bytes.Index(b, jsonEnd) |
| if end < 0 { |
| return |
| } |
| b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing } |
| if err = json.Unmarshal(b, &meta); err != nil { |
| return |
| } |
| tail = tail[end+len(jsonEnd):] |
| return |
| } |
| |
| // updateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata, |
| // and updates the DocMetadata map. |
| func (c *Corpus) updateMetadata() { |
| metadata := make(map[string]*Metadata) |
| var scan func(string) // scan is recursive |
| scan = func(dir string) { |
| fis, err := c.fs.ReadDir(dir) |
| if err != nil { |
| if dir == "/doc" && errors.Is(err, os.ErrNotExist) { |
| // Be quiet during tests that don't have a /doc tree. |
| return |
| } |
| log.Printf("updateMetadata %s: %v", dir, err) |
| return |
| } |
| for _, fi := range fis { |
| name := pathpkg.Join(dir, fi.Name()) |
| if fi.IsDir() { |
| scan(name) // recurse |
| continue |
| } |
| if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".md") { |
| continue |
| } |
| // Extract metadata from the file. |
| b, err := vfs.ReadFile(c.fs, name) |
| if err != nil { |
| log.Printf("updateMetadata %s: %v", name, err) |
| continue |
| } |
| meta, _, err := extractMetadata(b) |
| if err != nil { |
| log.Printf("updateMetadata: %s: %v", name, err) |
| continue |
| } |
| // Present all .md as if they were .html, |
| // so that it doesn't matter which one a page is written in. |
| if strings.HasSuffix(name, ".md") { |
| name = strings.TrimSuffix(name, ".md") + ".html" |
| } |
| // Store relative filesystem path in Metadata. |
| meta.filePath = name |
| if meta.Path == "" { |
| // If no Path, canonical path is actual path with .html removed. |
| meta.Path = strings.TrimSuffix(name, ".html") |
| } |
| // Store under both paths. |
| metadata[meta.Path] = &meta |
| metadata[meta.filePath] = &meta |
| for _, path := range meta.AltPaths { |
| metadata[path] = &meta |
| } |
| } |
| } |
| scan("/doc") |
| c.docMetadata.Set(metadata) |
| } |
| |
| // MetadataFor returns the *Metadata for a given relative path or nil if none |
| // exists. |
| func (c *Corpus) MetadataFor(relpath string) *Metadata { |
| if m, _ := c.docMetadata.Get(); m != nil { |
| meta := m.(map[string]*Metadata) |
| // If metadata for this relpath exists, return it. |
| if p := meta[relpath]; p != nil { |
| return p |
| } |
| // Try with or without trailing slash. |
| if strings.HasSuffix(relpath, "/") { |
| relpath = relpath[:len(relpath)-1] |
| } else { |
| relpath = relpath + "/" |
| } |
| return meta[relpath] |
| } |
| return nil |
| } |
| |
| // refreshMetadata sends a signal to update DocMetadata. If a refresh is in |
| // progress the metadata will be refreshed again afterward. |
| func (c *Corpus) refreshMetadata() { |
| select { |
| case c.refreshMetadataSignal <- true: |
| default: |
| } |
| } |
| |
| // refreshMetadataLoop runs forever, updating DocMetadata when the underlying |
| // file system changes. It should be launched in a goroutine. |
| func (c *Corpus) refreshMetadataLoop() { |
| for { |
| <-c.refreshMetadataSignal |
| c.updateMetadata() |
| time.Sleep(10 * time.Second) // at most once every 10 seconds |
| } |
| } |