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