--- /dev/null
+// 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
+
+// TODO(adg): why are some exported and some aren't? -brad
+type Metadata struct {
+ Title string
+ Subtitle string
+ Template bool // execute as template
+ Path string // canonical path for this page
+ 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 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") {
+ 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
+ }
+ // Store relative filesystem path in Metadata.
+ meta.filePath = name
+ if meta.Path == "" {
+ // If no Path, canonical path is actual path.
+ meta.Path = meta.filePath
+ }
+ // Store under both paths.
+ metadata[meta.Path] = &meta
+ metadata[meta.filePath] = &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
+ }
+}