mirror of
https://github.com/golang/go
synced 2024-11-24 22:47:58 -07:00
godoc: support canonical Paths in HTML metadata
Redirect to the canonical path when the old path is accessed. R=gri CC=golang-dev https://golang.org/cl/5536061
This commit is contained in:
parent
5a1322a79f
commit
8bbe5ccb71
@ -1,5 +1,6 @@
|
|||||||
<!--{
|
<!--{
|
||||||
"Title": "Documentation"
|
"Title": "Documentation",
|
||||||
|
"Path": "/doc/"
|
||||||
}-->
|
}-->
|
||||||
|
|
||||||
<div class="left-column">
|
<div class="left-column">
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
<!--{
|
||||||
|
"Path": "/"
|
||||||
|
}-->
|
||||||
|
|
||||||
<link rel="stylesheet" type="text/css" href="/doc/frontpage.css">
|
<link rel="stylesheet" type="text/css" href="/doc/frontpage.css">
|
||||||
|
|
||||||
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
|
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
|
||||||
|
@ -80,6 +80,7 @@ var (
|
|||||||
fsTree RWValue // *Directory tree of packages, updated with each sync
|
fsTree RWValue // *Directory tree of packages, updated with each sync
|
||||||
pathFilter RWValue // filter used when building fsMap directory trees
|
pathFilter RWValue // filter used when building fsMap directory trees
|
||||||
fsModified RWValue // timestamp of last call to invalidateIndex
|
fsModified RWValue // timestamp of last call to invalidateIndex
|
||||||
|
docMetadata RWValue // mapping from paths to *Metadata
|
||||||
|
|
||||||
// http handlers
|
// http handlers
|
||||||
fileServer http.Handler // default file server
|
fileServer http.Handler // default file server
|
||||||
@ -698,11 +699,6 @@ var (
|
|||||||
jsonEnd = []byte("}-->")
|
jsonEnd = []byte("}-->")
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metadata struct {
|
|
||||||
Title string
|
|
||||||
Subtitle string
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath string) {
|
||||||
// get HTML body contents
|
// get HTML body contents
|
||||||
src, err := ReadFile(fs, abspath)
|
src, err := ReadFile(fs, abspath)
|
||||||
@ -720,15 +716,9 @@ func serveHTMLDoc(w http.ResponseWriter, r *http.Request, abspath, relpath strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// if it begins with a JSON blob, read in the metadata.
|
// if it begins with a JSON blob, read in the metadata.
|
||||||
var meta Metadata
|
meta, src, err := extractMetadata(src)
|
||||||
if bytes.HasPrefix(src, jsonStart) {
|
if err != nil {
|
||||||
if end := bytes.Index(src, jsonEnd); end > -1 {
|
log.Printf("decoding metadata %s: %v", relpath, err)
|
||||||
b := src[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
|
|
||||||
if err := json.Unmarshal(b, &meta); err != nil {
|
|
||||||
log.Printf("decoding metadata for %s: %v", relpath, err)
|
|
||||||
}
|
|
||||||
src = src[end+len(jsonEnd):]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if it's the language spec, add tags to EBNF productions
|
// if it's the language spec, add tags to EBNF productions
|
||||||
@ -790,20 +780,21 @@ func serveDirectory(w http.ResponseWriter, r *http.Request, abspath, relpath str
|
|||||||
}
|
}
|
||||||
|
|
||||||
func serveFile(w http.ResponseWriter, r *http.Request) {
|
func serveFile(w http.ResponseWriter, r *http.Request) {
|
||||||
relpath := r.URL.Path[1:] // serveFile URL paths start with '/'
|
relpath := r.URL.Path
|
||||||
abspath := absolutePath(relpath, *goroot)
|
|
||||||
|
|
||||||
// pick off special cases and hand the rest to the standard file server
|
// Check to see if we need to redirect or serve another file.
|
||||||
switch r.URL.Path {
|
if m := metadataFor(relpath); m != nil {
|
||||||
case "/":
|
if m.Path != relpath {
|
||||||
serveHTMLDoc(w, r, filepath.Join(*goroot, "doc", "root.html"), "doc/root.html")
|
// Redirect to canonical path.
|
||||||
return
|
http.Redirect(w, r, m.Path, http.StatusMovedPermanently)
|
||||||
|
|
||||||
case "/doc/root.html":
|
|
||||||
// hide landing page from its real name
|
|
||||||
http.Redirect(w, r, "/", http.StatusMovedPermanently)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Serve from the actual filesystem path.
|
||||||
|
relpath = m.filePath
|
||||||
|
}
|
||||||
|
|
||||||
|
relpath = relpath[1:] // strip leading slash
|
||||||
|
abspath := absolutePath(relpath, *goroot)
|
||||||
|
|
||||||
switch path.Ext(relpath) {
|
switch path.Ext(relpath) {
|
||||||
case ".html":
|
case ".html":
|
||||||
@ -1303,6 +1294,120 @@ func search(w http.ResponseWriter, r *http.Request) {
|
|||||||
servePage(w, title, "", query, contents)
|
servePage(w, title, "", query, contents)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------------
|
||||||
|
// Documentation Metadata
|
||||||
|
|
||||||
|
type Metadata struct {
|
||||||
|
Title string
|
||||||
|
Subtitle string
|
||||||
|
Path string // canonical path for this page
|
||||||
|
filePath string // filesystem path relative to goroot
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 files, reads their metadata,
|
||||||
|
// and updates the docMetadata map.
|
||||||
|
//
|
||||||
|
func updateMetadata() {
|
||||||
|
metadata := make(map[string]*Metadata)
|
||||||
|
var scan func(string) // scan is recursive
|
||||||
|
scan = func(dir string) {
|
||||||
|
fis, err := fs.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("updateMetadata:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, fi := range fis {
|
||||||
|
name := filepath.Join(dir, fi.Name())
|
||||||
|
if fi.IsDir() {
|
||||||
|
scan(name) // recurse
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(name, ".html") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Extract metadata from the file.
|
||||||
|
b, err := ReadFile(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
|
||||||
|
}
|
||||||
|
// Store relative filesystem path in Metadata.
|
||||||
|
meta.filePath = filepath.Join("/", name[len(*goroot):])
|
||||||
|
if meta.Path == "" {
|
||||||
|
// If no Path, canonical path is actual path.
|
||||||
|
meta.Path = meta.filePath
|
||||||
|
}
|
||||||
|
// Store under both paths.
|
||||||
|
metadata[meta.Path] = &meta
|
||||||
|
metadata[meta.filePath] = &meta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scan(filepath.Join(*goroot, "doc"))
|
||||||
|
docMetadata.set(metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send a value on this channel to trigger a metadata refresh.
|
||||||
|
// It is buffered so that if a signal is not lost if sent during a refresh.
|
||||||
|
//
|
||||||
|
var refreshMetadataSignal = make(chan bool, 1)
|
||||||
|
|
||||||
|
// refreshMetadata sends a signal to update docMetadata. If a refresh is in
|
||||||
|
// progress the metadata will be refreshed again afterward.
|
||||||
|
//
|
||||||
|
func refreshMetadata() {
|
||||||
|
select {
|
||||||
|
case refreshMetadataSignal <- true:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// refreshMetadataLoop runs forever, updating docMetadata when the underlying
|
||||||
|
// file system changes. It should be launched in a goroutine by main.
|
||||||
|
//
|
||||||
|
func refreshMetadataLoop() {
|
||||||
|
for {
|
||||||
|
<-refreshMetadataSignal
|
||||||
|
updateMetadata()
|
||||||
|
time.Sleep(10 * time.Second) // at most once every 10 seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// metadataFor returns the *Metadata for a given relative path or nil if none
|
||||||
|
// exists.
|
||||||
|
//
|
||||||
|
func metadataFor(relpath string) *Metadata {
|
||||||
|
if m, _ := docMetadata.get(); m != nil {
|
||||||
|
return m.(map[string]*Metadata)[relpath]
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------------
|
// ----------------------------------------------------------------------------
|
||||||
// Indexer
|
// Indexer
|
||||||
|
|
||||||
@ -1311,6 +1416,7 @@ func search(w http.ResponseWriter, r *http.Request) {
|
|||||||
//
|
//
|
||||||
func invalidateIndex() {
|
func invalidateIndex() {
|
||||||
fsModified.set(nil)
|
fsModified.set(nil)
|
||||||
|
refreshMetadata()
|
||||||
}
|
}
|
||||||
|
|
||||||
// indexUpToDate() returns true if the search index is not older
|
// indexUpToDate() returns true if the search index is not older
|
||||||
|
@ -337,6 +337,9 @@ func main() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Periodically refresh metadata.
|
||||||
|
go refreshMetadataLoop()
|
||||||
|
|
||||||
// Initialize search index.
|
// Initialize search index.
|
||||||
if *indexEnabled {
|
if *indexEnabled {
|
||||||
go indexer()
|
go indexer()
|
||||||
|
Loading…
Reference in New Issue
Block a user