github.com/quay/claircore@v1.5.28/internal/mdbook/mdbook.go (about)

     1  // Package mdbook is a helper for writing mdbook plugins.
     2  package mdbook
     3  
     4  import (
     5  	"context"
     6  	"encoding/json"
     7  	"fmt"
     8  	"io"
     9  	"log"
    10  	"os"
    11  	"os/signal"
    12  	"strings"
    13  )
    14  
    15  // Main is a simple replacement main for mdbook preprocessors.
    16  func Main(name string, pf ProcFunc) {
    17  	log.SetFlags(log.LstdFlags | log.Lmsgprefix)
    18  	log.SetPrefix(name + ": ")
    19  	Args(os.Args)
    20  
    21  	err := func() error {
    22  		ctx := context.Background()
    23  		ctx, cancel := signal.NotifyContext(ctx, os.Interrupt, os.Kill)
    24  		defer cancel()
    25  
    26  		cfg, book, err := Decode(os.Stdin)
    27  		if err != nil {
    28  			return err
    29  		}
    30  		proc, err := pf(ctx, cfg)
    31  		if err != nil {
    32  			return err
    33  		}
    34  
    35  		if err := proc.Walk(ctx, book); err != nil {
    36  			return err
    37  		}
    38  
    39  		if err := json.NewEncoder(os.Stdout).Encode(&book); err != nil {
    40  			return err
    41  		}
    42  		return nil
    43  	}()
    44  	if err != nil {
    45  		log.Fatal(err)
    46  	}
    47  }
    48  
    49  // ProcFunc is a hook for creating a new Proc.
    50  type ProcFunc func(context.Context, *Context) (*Proc, error)
    51  
    52  // Args implements handling the expected CLI arguments.
    53  //
    54  // This function calls [os.Exit] under the right circumstances.
    55  func Args(argv []string) {
    56  	// Handle when called with "supports $renderer".
    57  	if len(argv) != 3 {
    58  		return
    59  	}
    60  	switch argv[1] {
    61  	case "supports":
    62  		switch argv[2] {
    63  		case "html":
    64  		default:
    65  			log.Printf("unsupported renderer: %q", argv[2])
    66  			os.Exit(1)
    67  		}
    68  	default:
    69  		log.Printf("unknown subcommand: %q", argv[1])
    70  		os.Exit(1)
    71  	}
    72  	os.Exit(0)
    73  }
    74  
    75  // Decode reads the [Context] and [Book] JSON objects from the passed
    76  // [io.Reader]. This should be almost always be [os.Stdin].
    77  func Decode(r io.Reader) (*Context, *Book, error) {
    78  	dec := json.NewDecoder(r)
    79  	tok, err := dec.Token()
    80  	if err != nil {
    81  		return nil, nil, err
    82  	}
    83  	if r, ok := tok.(json.Delim); !ok || r != '[' {
    84  		return nil, nil, fmt.Errorf("unexpected start of input: %v", tok)
    85  	}
    86  
    87  	var ppContext Context
    88  	if err := dec.Decode(&ppContext); err != nil {
    89  		return nil, nil, err
    90  	}
    91  	var book Book
    92  	if err := dec.Decode(&book); err != nil {
    93  		return nil, nil, err
    94  	}
    95  
    96  	tok, err = dec.Token()
    97  	if err != nil {
    98  		return nil, nil, err
    99  	}
   100  	if r, ok := tok.(json.Delim); !ok || r != ']' {
   101  		return nil, nil, fmt.Errorf("unexpected end of input: %v", tok)
   102  	}
   103  
   104  	return &ppContext, &book, nil
   105  }
   106  
   107  // Proc is a helper for modifying a [Book].
   108  type Proc struct {
   109  	Chapter   Hook[Chapter]
   110  	Separator Hook[Separator]
   111  	PartTitle Hook[PartTitle]
   112  }
   113  
   114  // Hook is a hook function to modify a BookItem in-place.
   115  type Hook[I BookItem] func(ctx context.Context, b *strings.Builder, item *I) error
   116  
   117  // BookItem is one of [Chapter], [Separator], or [PartTitle].
   118  type BookItem interface {
   119  	Chapter | Separator | PartTitle
   120  }
   121  
   122  // Walk walks the provided [Book], calling the [Hook]s in the member fields as
   123  // needed to modify elements in-place.
   124  func (p *Proc) Walk(ctx context.Context, book *Book) error {
   125  	var b strings.Builder
   126  	var err error
   127  	for _, sec := range book.Sections {
   128  		err = p.section(ctx, &b, sec)
   129  		if err != nil {
   130  			return err
   131  		}
   132  	}
   133  	return nil
   134  }
   135  
   136  // Section calls the relevant hooks on the current [Section].
   137  func (p *Proc) section(ctx context.Context, b *strings.Builder, s Section) (err error) {
   138  	b.Reset()
   139  	switch {
   140  	case s.Separator != nil:
   141  		if p.Separator != nil {
   142  			err = p.Separator(ctx, b, s.Separator)
   143  		}
   144  	case s.PartTitle != nil:
   145  		if p.PartTitle != nil {
   146  			err = p.PartTitle(ctx, b, s.PartTitle)
   147  		}
   148  	case s.Chapter != nil:
   149  		if p.Chapter != nil {
   150  			err = p.Chapter(ctx, b, s.Chapter)
   151  			if err != nil {
   152  				break
   153  			}
   154  		}
   155  		for _, s := range s.Chapter.SubItems {
   156  			err = p.section(ctx, b, s)
   157  			if err != nil {
   158  				break
   159  			}
   160  		}
   161  	}
   162  	if err != nil {
   163  		return err
   164  	}
   165  	if err := ctx.Err(); err != nil {
   166  		return context.Cause(ctx)
   167  	}
   168  	return nil
   169  }
   170  
   171  // Context is the whole mdbook context.
   172  type Context struct {
   173  	Root     string `json:"root"`
   174  	Renderer string `json:"renderer"`
   175  	Version  string `json:"mdbook_version"`
   176  	Config   struct {
   177  		Book   BookConfig                 `json:"book"`
   178  		Output map[string]json.RawMessage `json:"output"`
   179  	} `json:"config"`
   180  	Preprocessor map[string]json.RawMessage `json:"preprocessor"`
   181  }
   182  
   183  // BookConfig is the mdbook metadata and configuration.
   184  type BookConfig struct {
   185  	Authors      []string `json:"authors"`
   186  	Source       string   `json:"src"`
   187  	Description  string   `json:"description"`
   188  	Language     string   `json:"language"`
   189  	Title        string   `json:"title"`
   190  	MultiLingual bool     `json:"multilingual"`
   191  }
   192  
   193  // Book is an mdbook book.
   194  type Book struct {
   195  	Sections []Section `json:"sections"`
   196  	X        *struct{} `json:"__non_exhaustive"`
   197  }
   198  
   199  // Section is one of a [Chapter], [Separator], or [PartTitle].
   200  type Section struct {
   201  	Chapter   *Chapter   `json:",omitempty"`
   202  	Separator *Separator `json:",omitempty"`
   203  	PartTitle *PartTitle `json:",omitempty"`
   204  }
   205  
   206  // Separator denotes an mdbook separator.
   207  type Separator struct{}
   208  
   209  // PartTitle is the title of the current part.
   210  type PartTitle string
   211  
   212  // Chapter is an mdbook chapter.
   213  type Chapter struct {
   214  	Name        string    `json:"name"`
   215  	Content     string    `json:"content"`
   216  	Number      []int     `json:"number"`
   217  	SubItems    []Section `json:"sub_items"`
   218  	Path        *string   `json:"path"`
   219  	SourcePath  *string   `json:"source_path"`
   220  	ParentNames []string  `json:"parent_names"`
   221  }