aboutsummaryrefslogtreecommitdiff
path: root/godoc/meta.go
blob: 76a27508b68a760b75fa6db8cda95b1148e0cc9b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
// 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
	}
}