...

Source file src/golang.org/x/tools/godoc/meta.go

Documentation: golang.org/x/tools/godoc

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

View as plain text