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