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 }