blob: 14ff6b8b70fb339002058d1e9cfca92ff9c2d9a9 [file] [log] [blame]
// 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.
//go:build go1.16
// +build go1.16
package godoc
import (
"bytes"
"encoding/json"
"errors"
"io/fs"
"log"
"os"
"path"
"strings"
"time"
)
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 := fs.ReadDir(c.fs, toFS(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 := path.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 := fs.ReadFile(c.fs, toFS(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
}
}