github.com/v2fly/tools@v0.100.0/godoc/meta.go (about)

     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  	"github.com/v2fly/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  //
    47  func extractMetadata(b []byte) (meta Metadata, tail []byte, err error) {
    48  	tail = b
    49  	if !bytes.HasPrefix(b, jsonStart) {
    50  		return
    51  	}
    52  	end := bytes.Index(b, jsonEnd)
    53  	if end < 0 {
    54  		return
    55  	}
    56  	b = b[len(jsonStart)-1 : end+1] // drop leading <!-- and include trailing }
    57  	if err = json.Unmarshal(b, &meta); err != nil {
    58  		return
    59  	}
    60  	tail = tail[end+len(jsonEnd):]
    61  	return
    62  }
    63  
    64  // UpdateMetadata scans $GOROOT/doc for HTML and Markdown files, reads their metadata,
    65  // and updates the DocMetadata map.
    66  func (c *Corpus) updateMetadata() {
    67  	metadata := make(map[string]*Metadata)
    68  	var scan func(string) // scan is recursive
    69  	scan = func(dir string) {
    70  		fis, err := c.fs.ReadDir(dir)
    71  		if err != nil {
    72  			if dir == "/doc" && errors.Is(err, os.ErrNotExist) {
    73  				// Be quiet during tests that don't have a /doc tree.
    74  				return
    75  			}
    76  			log.Printf("updateMetadata %s: %v", dir, err)
    77  			return
    78  		}
    79  		for _, fi := range fis {
    80  			name := pathpkg.Join(dir, fi.Name())
    81  			if fi.IsDir() {
    82  				scan(name) // recurse
    83  				continue
    84  			}
    85  			if !strings.HasSuffix(name, ".html") && !strings.HasSuffix(name, ".md") {
    86  				continue
    87  			}
    88  			// Extract metadata from the file.
    89  			b, err := vfs.ReadFile(c.fs, name)
    90  			if err != nil {
    91  				log.Printf("updateMetadata %s: %v", name, err)
    92  				continue
    93  			}
    94  			meta, _, err := extractMetadata(b)
    95  			if err != nil {
    96  				log.Printf("updateMetadata: %s: %v", name, err)
    97  				continue
    98  			}
    99  			// Present all .md as if they were .html,
   100  			// so that it doesn't matter which one a page is written in.
   101  			if strings.HasSuffix(name, ".md") {
   102  				name = strings.TrimSuffix(name, ".md") + ".html"
   103  			}
   104  			// Store relative filesystem path in Metadata.
   105  			meta.filePath = name
   106  			if meta.Path == "" {
   107  				// If no Path, canonical path is actual path with .html removed.
   108  				meta.Path = strings.TrimSuffix(name, ".html")
   109  			}
   110  			// Store under both paths.
   111  			metadata[meta.Path] = &meta
   112  			metadata[meta.filePath] = &meta
   113  			for _, path := range meta.AltPaths {
   114  				metadata[path] = &meta
   115  			}
   116  		}
   117  	}
   118  	scan("/doc")
   119  	c.docMetadata.Set(metadata)
   120  }
   121  
   122  // MetadataFor returns the *Metadata for a given relative path or nil if none
   123  // exists.
   124  //
   125  func (c *Corpus) MetadataFor(relpath string) *Metadata {
   126  	if m, _ := c.docMetadata.Get(); m != nil {
   127  		meta := m.(map[string]*Metadata)
   128  		// If metadata for this relpath exists, return it.
   129  		if p := meta[relpath]; p != nil {
   130  			return p
   131  		}
   132  		// Try with or without trailing slash.
   133  		if strings.HasSuffix(relpath, "/") {
   134  			relpath = relpath[:len(relpath)-1]
   135  		} else {
   136  			relpath = relpath + "/"
   137  		}
   138  		return meta[relpath]
   139  	}
   140  	return nil
   141  }
   142  
   143  // refreshMetadata sends a signal to update DocMetadata. If a refresh is in
   144  // progress the metadata will be refreshed again afterward.
   145  //
   146  func (c *Corpus) refreshMetadata() {
   147  	select {
   148  	case c.refreshMetadataSignal <- true:
   149  	default:
   150  	}
   151  }
   152  
   153  // RefreshMetadataLoop runs forever, updating DocMetadata when the underlying
   154  // file system changes. It should be launched in a goroutine.
   155  func (c *Corpus) refreshMetadataLoop() {
   156  	for {
   157  		<-c.refreshMetadataSignal
   158  		c.updateMetadata()
   159  		time.Sleep(10 * time.Second) // at most once every 10 seconds
   160  	}
   161  }