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.
17 "golang.org/x/tools/godoc/vfs"
21 doctype = []byte("<!DOCTYPE ")
22 jsonStart = []byte("<!--{")
23 jsonEnd = []byte("}-->")
26 // ----------------------------------------------------------------------------
27 // Documentation Metadata
29 type Metadata struct {
30 // These fields can be set in the JSON header at the top of a doc.
33 Template bool // execute as template
34 Path string // canonical path for this page
35 AltPaths []string // redirect these other paths to this page
37 // These are internal to the implementation.
38 filePath string // filesystem path relative to goroot
41 func (m *Metadata) FilePath() string { return m.filePath }
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.
47 func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
49 if !bytes.HasPrefix(b, jsonStart) {
52 end := bytes.Index(b, jsonEnd)
56 b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
57 if err = json.Unmarshal(b, &meta); err != nil {
60 tail = tail[end+len(jsonEnd):]
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)
72 if dir == "/doc" && errors.Is(err, os.ErrNotExist) {
73 // Be quiet during tests that don't have a /doc tree.
76 log.Printf("updateMetadata %s: %v", dir, err)
79 for _, fi := range fis {
80 name := pathpkg.Join(dir, fi.Name())
85 if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".md") {
88 // Extract metadata from the file.
89 b, err := vfs.ReadFile(c.fs, name)
91 log.Printf("updateMetadata %s: %v", name, err)
94 meta, _, err := extractMetadata(b)
96 log.Printf("updateMetadata: %s: %v", name, err)
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"
104 // Store relative filesystem path in Metadata.
107 // If no Path, canonical path is actual path with .html removed.
108 meta.Path = strings.TrimSuffix(name, ".html")
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
119 c.docMetadata.Set(metadata)
122 // MetadataFor returns the *Metadata for a given relative path or nil if none
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 {
132 // Try with or without trailing slash.
133 if strings.HasSuffix(relpath, "/") {
134 relpath = relpath[:len(relpath)-1]
136 relpath = relpath + "/"
143 // refreshMetadata sends a signal to update DocMetadata. If a refresh is in
144 // progress the metadata will be refreshed again afterward.
146 func (c *Corpus) refreshMetadata() {
148 case c.refreshMetadataSignal <- true:
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() {
157 <-c.refreshMetadataSignal
159 time.Sleep(10 * time.Second) // at most once every 10 seconds