зеркало из https://github.com/golang/tools.git
159 строки
4.2 KiB
Go
159 строки
4.2 KiB
Go
// 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
|
|
}
|
|
}
|