[READ-ONLY] Mirror of https://github.com/jphastings/mela-recipes. An opinionated library for stream-parsing Mela's recipe files.
cooking cooking-recipes mela recipes
0

Configure Feed

Select the types of activity you want to include in your feed.

Functioning blocking conversion

+1063 -71
+1
.gitignore
··· 2 2 /README_test.go 3 3 /dist/ 4 4 /test-results.json 5 + .env
+39 -1
cmd/recipes/convert.go
··· 2 2 3 3 import ( 4 4 "fmt" 5 + "os" 5 6 "strings" 6 7 7 8 "github.com/jphastings/recipes" 9 + "github.com/jphastings/recipes/internal/formats" 10 + "github.com/jphastings/recipes/internal/standardize" 8 11 "github.com/spf13/cobra" 9 12 ) 10 13 ··· 14 17 Short: "Convert recipes between different formats", 15 18 Long: longExplain(), 16 19 RunE: func(cmd *cobra.Command, args []string) error { 17 - rs, rc, err := recipes.ParseAll(args) 20 + o, err := formats.LoadOptions() 21 + if err != nil { 22 + return err 23 + } 24 + 25 + rs, rc, err := recipes.ParseAll(args, o) 18 26 if err != nil { 19 27 return err 28 + } 29 + 30 + to, err := cmd.Flags().GetString("to") 31 + if err != nil { 32 + return err 33 + } 34 + filename, asType, destFormat := recipes.ParseDestination(to) 35 + 36 + makeCollection := asType == recipes.AsTypeCollection || asType == recipes.AsTypeAny && rc != nil 37 + if makeCollection { 38 + rcName := "Recipes" 39 + if rc != nil { 40 + rcName = rc.Name() 41 + } 42 + 43 + out, err := destFormat.NewCollection(rcName, rc.Recipes()) 44 + if err != nil { 45 + return fmt.Errorf("unable to create a new collection in the %s format: %w", destFormat.Name, err) 46 + } 47 + 48 + if filename == "" { 49 + filename = standardize.StringToFilename(rcName) + destFormat.ExtensionCollection 50 + } 51 + 52 + f, err := os.Create(filename) 53 + if err != nil { 54 + return fmt.Errorf("unable to make output recipe collection: %w", err) 55 + } 56 + 57 + return out.Marshal(f) 20 58 } 21 59 22 60 fmt.Println(len(rs), rc)
+6 -6
cooklang/format.go
··· 11 11 ) 12 12 13 13 const ( 14 - collectionExt = ".cook" 14 + recipeExt = ".cook" 15 15 ) 16 16 17 - var FormatInfo = formats.Format{ 17 + var FormatInfo = &formats.Format{ 18 18 Name: "Cooklang", 19 19 URL: "https://cooklang.org", 20 20 Features: formats.Features{ 21 21 ParseRecipe: true, 22 22 }, 23 - ExtensionCollection: collectionExt, 24 - Parse: func(formats.Bundle) (formats.Recipe, formats.RecipeCollection, error) { 23 + Extension: recipeExt, 24 + Parse: func(formats.Bundle, formats.ParseOptions) (formats.Recipe, formats.RecipeCollection, error) { 25 25 return nil, nil, fmt.Errorf("cooklang parsing not yet implemented") 26 26 }, 27 27 Bundle: bundle, 28 28 } 29 29 30 - var bundleExts = []string{collectionExt, ".jpg", ".jpeg", ".png"} 30 + var bundleExts = []string{recipeExt, ".jpg", ".jpeg", ".png"} 31 31 var sectionSuffix = regexp.MustCompile(`\.\d+$`) 32 32 33 33 func bundle(files []string) (bundles []formats.Bundle, unused []string) { ··· 40 40 continue 41 41 } 42 42 k := strings.TrimSuffix(f, ext) 43 - if ext != collectionExt { 43 + if ext != recipeExt { 44 44 k = sectionSuffix.ReplaceAllString(k, "") 45 45 } 46 46 idx[k] = append(idx[k], f)
+3 -3
crouton/format.go
··· 8 8 9 9 const recipeExt = ".crumb" 10 10 11 - var FormatInfo = formats.Format{ 11 + var FormatInfo = &formats.Format{ 12 12 Name: "Crouton", 13 13 URL: "https://crouton.app", 14 14 Features: formats.Features{ ··· 16 16 WriteRecipe: true, 17 17 }, 18 18 Extension: recipeExt, 19 - New: newFromInterchange, 20 - Parse: func(formats.Bundle) (formats.Recipe, formats.RecipeCollection, error) { 19 + New: importRecipe, 20 + Parse: func(formats.Bundle, formats.ParseOptions) (formats.Recipe, formats.RecipeCollection, error) { 21 21 return nil, nil, fmt.Errorf("crouton parsing not yet implemented") 22 22 }, 23 23 Bundle: formats.BundleByExtension(recipeExt),
+4 -6
crouton/import.go
··· 1 1 package crouton 2 2 3 3 import ( 4 + "fmt" 5 + 4 6 "github.com/jphastings/recipes/internal/formats" 5 7 ) 6 8 7 - func newFromInterchange(ir formats.InterchangeRecipe) (formats.Recipe, error) { 8 - return &Recipe{ 9 - filename: ir.Filename, 10 - RecipeName: ir.Title, 11 - // TODO: Other fields 12 - }, nil 9 + func importRecipe(ir formats.Recipe) (formats.Recipe, error) { 10 + return nil, fmt.Errorf("importing to crouton not yet supported") 13 11 }
+3 -3
crouton/recipe.go
··· 28 28 SenderName string `json:"senderName"` 29 29 } 30 30 31 - func (r Recipe) Name() string { return r.RecipeName } 32 - func (r Recipe) Format() formats.Format { return FormatInfo } 33 - func (r Recipe) Filename() string { return r.filename + FormatInfo.Extension } 31 + func (r Recipe) Name() string { return r.RecipeName } 32 + func (r Recipe) Format() *formats.Format { return FormatInfo } 33 + func (r Recipe) Filename() string { return r.filename + FormatInfo.Extension } 34 34 35 35 type Link string 36 36
+11
epub/export.go
··· 1 + package epub 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/jphastings/recipes/internal/formats" 7 + ) 8 + 9 + func (r RecipeCollection) Export() (formats.InterchangeRecipe, error) { 10 + return formats.InterchangeRecipe{}, fmt.Errorf("export of epub recipe format not yet implemented") 11 + }
+3 -6
epub/format.go
··· 1 1 package epub 2 2 3 3 import ( 4 - "fmt" 5 - 6 4 "github.com/jphastings/recipes/internal/formats" 7 5 ) 8 6 ··· 10 8 collectionExt = ".epub" 11 9 ) 12 10 13 - var FormatInfo = formats.Format{ 11 + var FormatInfo = &formats.Format{ 14 12 Name: "ePub", 15 13 URL: "https://en.wikipedia.org/wiki/EPUB", 16 14 Features: formats.Features{ 17 15 ParseCollection: true, 18 16 }, 19 17 ExtensionCollection: collectionExt, 20 - Parse: func(formats.Bundle) (formats.Recipe, formats.RecipeCollection, error) { 21 - return nil, nil, fmt.Errorf("ePub parsing not yet implemented") 22 - }, 18 + 19 + Parse: Parse, 23 20 Bundle: formats.BundleByExtension(collectionExt), 24 21 }
+51
epub/ncx.go
··· 1 + package epub 2 + 3 + import ( 4 + "encoding/xml" 5 + "io" 6 + ) 7 + 8 + type NCX struct { 9 + XMLName xml.Name `xml:"ncx"` 10 + Version string `xml:"version,attr"` 11 + DocTitle DocTitle `xml:"docTitle"` 12 + DocAuthor DocAuthor `xml:"docAuthor"` 13 + NavMap NavMap `xml:"navMap"` 14 + } 15 + 16 + type DocTitle struct { 17 + Text string `xml:"text"` 18 + } 19 + 20 + type DocAuthor struct { 21 + Text string `xml:"text"` 22 + } 23 + 24 + type NavMap struct { 25 + NavPoints []NavPoint `xml:"navPoint"` 26 + } 27 + 28 + type NavPoint struct { 29 + ID string `xml:"id,attr"` 30 + PlayOrder string `xml:"playOrder,attr"` 31 + Label NavLabel `xml:"navLabel"` 32 + Content NavContent `xml:"content"` 33 + Children []NavPoint `xml:"navPoint"` // Supports nested navPoints. 34 + } 35 + 36 + type NavLabel struct { 37 + Text string `xml:"text"` 38 + } 39 + 40 + type NavContent struct { 41 + Src string `xml:"src,attr"` 42 + } 43 + 44 + func parseNCX(r io.Reader) (NCX, error) { 45 + var ncx NCX 46 + err := xml.NewDecoder(r).Decode(&ncx) 47 + if err != nil { 48 + return NCX{}, err 49 + } 50 + return ncx, nil 51 + }
+261
epub/parse.go
··· 1 + package epub 2 + 3 + import ( 4 + _ "embed" 5 + "fmt" 6 + "path" 7 + "regexp" 8 + "slices" 9 + "sort" 10 + "strings" 11 + 12 + "github.com/jphastings/recipes/internal/formats" 13 + "github.com/jphastings/recipes/internal/llm" 14 + "github.com/jphastings/recipes/utils" 15 + "golang.org/x/net/html" 16 + 17 + "github.com/antchfx/htmlquery" 18 + "github.com/pirmd/epub" 19 + ) 20 + 21 + func Parse(b formats.Bundle, o formats.ParseOptions) (formats.Recipe, formats.RecipeCollection, error) { 22 + if o.LLM == nil { 23 + return nil, nil, fmt.Errorf("the ePub parser requires an LLM to be configured") 24 + } 25 + 26 + filename := b[0] 27 + if path.Ext(filename) != collectionExt { 28 + return nil, nil, fmt.Errorf("doesn't appear to be an ePub file") 29 + } 30 + 31 + e, err := epub.Open(filename) 32 + if err != nil { 33 + return nil, nil, fmt.Errorf("couldn't open ePub: %w", err) 34 + } 35 + 36 + rc := &RecipeCollection{ 37 + filename: filename, 38 + } 39 + 40 + // Use the ePub's title, if it's in the standard place 41 + if i, err := e.Information(); err == nil { 42 + rc.name = i.Title[0] 43 + } 44 + 45 + if err := extractRecipes(e, rc, o.LLM); err != nil { 46 + return nil, nil, err 47 + } 48 + 49 + return nil, rc, nil 50 + } 51 + 52 + const extractRecipesPrompt = "The user will provide an HTML fragment that represents one or more cooking recipes. Your job is to convert it into the JSON structured recipe format provided as accurately as possible. You must fill out each section according to the descriptions of the JSON Schema." 53 + 54 + func extractRecipes(e *epub.Epub, rc *RecipeCollection, c *llm.Connection) error { 55 + index, err := getIndexFiles(e) 56 + if err != nil { 57 + return err 58 + } 59 + 60 + pm, err := getPageRef(e, index) 61 + if err != nil { 62 + return err 63 + } 64 + 65 + for filename, fragMap := range pm { 66 + f, err := e.OpenItem(filename) 67 + if err != nil { 68 + return fmt.Errorf("unable to open page within ePub (%s): %w", filename, err) 69 + } 70 + 71 + doc, err := html.Parse(f) 72 + if err != nil { 73 + return fmt.Errorf("unable to read page within ePub (%s): %w", filename, err) 74 + } 75 + 76 + fragments := makeRecipeHTMLFragments(doc, fragMap) 77 + fragments = fragments[0:1] 78 + 79 + for _, frag := range fragments { 80 + var recipes []formats.InterchangeRecipe 81 + if err := c.StructuredQuery(extractRecipesPrompt, frag.html, formats.RecipesSchema, &recipes); err != nil { 82 + return err 83 + } 84 + rc.recipes = append(rc.recipes, recipes...) 85 + } 86 + break 87 + } 88 + 89 + return nil 90 + } 91 + 92 + var otherIndexMatcher = regexp.MustCompile(`(?i)^(.*index.*)\d+(\.x?html)$`) 93 + 94 + // Finds the "index" page of the ePub (ie. what a human would use to look up what recipe is on what page) 95 + // Must guess if the recipe is spread across multiple HTML pages and, in that case, assumes that each of them has the same filename prefix and a numeric suffix 96 + func getIndexFiles(e *epub.Epub) ([]string, error) { 97 + pkg, err := e.Package() 98 + if err != nil { 99 + return nil, fmt.Errorf("couldn't open ePub package file: %w", err) 100 + } 101 + 102 + var tocFile string 103 + for _, item := range pkg.Manifest.Items { 104 + if item.MediaType == "application/x-dtbncx+xml" { 105 + tocFile = item.Href 106 + break 107 + } 108 + } 109 + if tocFile == "" { 110 + return nil, fmt.Errorf("no table of contents listed in the ePub package") 111 + } 112 + 113 + f, err := e.OpenItem(tocFile) 114 + if err != nil { 115 + return nil, fmt.Errorf("unable to open the table of contents file") 116 + } 117 + 118 + ncx, err := parseNCX(f) 119 + if err != nil { 120 + return nil, fmt.Errorf("couldn't read ePub's table of contents: %w", err) 121 + } 122 + 123 + var indexFiles []string 124 + var checkForOthers []string 125 + 126 + for _, p := range ncx.NavMap.NavPoints { 127 + text := strings.TrimSpace(strings.ToLower(p.Label.Text)) 128 + if text == "index" { 129 + indexFiles = append(indexFiles, p.Content.Src) 130 + checkForOthers = otherIndexMatcher.FindStringSubmatch(p.Content.Src) 131 + break 132 + } 133 + } 134 + 135 + if checkForOthers == nil { 136 + return indexFiles, nil 137 + } 138 + 139 + otherMatcher, err := regexp.Compile(fmt.Sprintf( 140 + `^%s\d+%s$`, 141 + regexp.QuoteMeta(checkForOthers[1]), 142 + regexp.QuoteMeta(checkForOthers[2]), 143 + )) 144 + if err != nil { 145 + return indexFiles, nil 146 + } 147 + 148 + for _, item := range pkg.Manifest.Items { 149 + if item.Href != checkForOthers[0] && otherMatcher.MatchString(item.Href) { 150 + indexFiles = append(indexFiles, item.Href) 151 + } 152 + } 153 + 154 + return indexFiles, nil 155 + } 156 + 157 + // index filename -> HTML id -> page number 158 + type realPageMap map[string]map[string]utils.Pages 159 + 160 + // Creates a map of the real page number of every reference to one in the provided index pages (likely a recipe), as well as the HTML fragment that it points to 161 + func getPageRef(e *epub.Epub, indexFiles []string) (realPageMap, error) { 162 + pm := make(realPageMap) 163 + for _, idx := range indexFiles { 164 + f, err := e.OpenItem(idx) 165 + if err != nil { 166 + return nil, fmt.Errorf("unable to open Index (%s): %w", idx, err) 167 + } 168 + 169 + doc, err := htmlquery.Parse(f) 170 + if err != nil { 171 + return nil, fmt.Errorf("unable to parse HTML in index: %w", err) 172 + } 173 + 174 + links, err := htmlquery.QueryAll(doc, "//a[text()]") 175 + if err != nil { 176 + return nil, fmt.Errorf("unable to find page references in the index: %w", err) 177 + } 178 + 179 + for _, link := range links { 180 + destParts := strings.Split(getHrefAttribute(link), "#") 181 + // Ignore links that don't have a fragment 182 + if len(destParts) != 2 { 183 + continue 184 + } 185 + // Ignore links to the index 186 + destPath := path.Join(path.Dir(idx), destParts[0]) 187 + if slices.Contains(indexFiles, destPath) { 188 + continue 189 + } 190 + text := htmlquery.InnerText(link) 191 + pages, err := utils.ParsePages(text) 192 + if err != nil { 193 + continue 194 + } 195 + 196 + if _, ok := pm[destPath]; !ok { 197 + pm[destPath] = make(map[string]utils.Pages) 198 + } 199 + pm[destPath][destParts[1]] = pages 200 + } 201 + } 202 + 203 + return pm, nil 204 + } 205 + 206 + // Pulls the href attribute value from an HTML node 207 + func getHrefAttribute(node *html.Node) string { 208 + if node.Type == html.ElementNode { 209 + for _, attr := range node.Attr { 210 + if attr.Key == "href" { 211 + return attr.Val 212 + } 213 + } 214 + } 215 + return "" 216 + } 217 + 218 + const fragmentTargetXPath = "//*[@id = '%s' or (local-name() = 'a' and @name = '%s')]" 219 + 220 + type htmlFragmentWithPages struct { 221 + html string 222 + pages utils.Pages 223 + } 224 + 225 + func makeRecipeHTMLFragments(doc *html.Node, fragMap map[string]utils.Pages) []htmlFragmentWithPages { 226 + var splitIndices []int 227 + idxMap := make(map[int]utils.Pages) 228 + docStr := htmlquery.OutputHTML(doc, true) 229 + 230 + for frag, pgs := range fragMap { 231 + node, err := htmlquery.Query(doc, fmt.Sprintf(fragmentTargetXPath, frag, frag)) 232 + if err != nil { 233 + fmt.Println("Nope on", frag) 234 + continue 235 + } 236 + 237 + idx := strings.Index(docStr, htmlquery.OutputHTML(node, true)) 238 + idxMap[idx] = pgs 239 + 240 + splitIndices = append(splitIndices, idx) 241 + } 242 + 243 + sort.Ints(splitIndices) 244 + 245 + fragments := make([]htmlFragmentWithPages, len(splitIndices)) 246 + for i, idx := range splitIndices { 247 + if i == len(splitIndices)-1 { 248 + fragments[i] = htmlFragmentWithPages{ 249 + html: docStr[idx:], 250 + pages: idxMap[idx], 251 + } 252 + } else { 253 + fragments[i] = htmlFragmentWithPages{ 254 + html: docStr[idx:splitIndices[i+1]], 255 + pages: idxMap[idx], 256 + } 257 + } 258 + } 259 + 260 + return fragments 261 + }
+36
epub/recipes.go
··· 1 + package epub 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + 7 + "github.com/jphastings/recipes/internal/formats" 8 + ) 9 + 10 + var _ formats.RecipeCollection = (*RecipeCollection)(nil) 11 + 12 + type RecipeCollection struct { 13 + name string 14 + filename string 15 + // ePub is not an output-capable format, so the internal storage method is actually the interchange format 16 + recipes []formats.InterchangeRecipe 17 + } 18 + 19 + func (rc *RecipeCollection) Filename() string { return rc.filename + FormatInfo.ExtensionCollection } 20 + func (rc *RecipeCollection) Format() *formats.Format { return FormatInfo } 21 + func (rc *RecipeCollection) Name() string { return rc.name } 22 + func (rc *RecipeCollection) Recipes() []formats.Recipe { 23 + out := make([]formats.Recipe, len(rc.recipes)) 24 + for i, r := range rc.recipes { 25 + out[i] = formats.Recipe(r) 26 + } 27 + return out 28 + } 29 + 30 + func (rc *RecipeCollection) Add(rs ...formats.Recipe) error { 31 + return fmt.Errorf("the ePub format doesn't support adding recipes") 32 + } 33 + 34 + func (rc *RecipeCollection) Marshal(io.Writer) error { 35 + return fmt.Errorf("writing recipes to the ePub format is not supported") 36 + }
+36 -4
formats.go
··· 1 1 package recipes 2 2 3 3 import ( 4 + "path" 5 + "strings" 6 + 4 7 "github.com/jphastings/recipes/cooklang" 5 8 "github.com/jphastings/recipes/crouton" 6 9 "github.com/jphastings/recipes/epub" ··· 8 11 "github.com/jphastings/recipes/mela" 9 12 ) 10 13 11 - func AvailableFormats() []formats.Format { 12 - return []formats.Format{ 14 + func AvailableFormats() []*formats.Format { 15 + return []*formats.Format{ 13 16 mela.FormatInfo, 14 17 crouton.FormatInfo, 15 18 epub.FormatInfo, ··· 19 22 20 23 // Attempts to find a suitable format & parser for all the recipe files given. All found recipes are returned in the first argument, the second argument holds the details of the collection if the input files represent *exactly and only one* collection. 21 24 // If no collections are represented, or the files represent a collection *and* other recipes or collections, then this second return value will be nil. 22 - func ParseAll(files []string) ([]formats.Recipe, formats.RecipeCollection, error) { 25 + func ParseAll(files []string, o formats.ParseOptions) ([]formats.Recipe, formats.RecipeCollection, error) { 23 26 countCollections := 0 24 27 var soloCollection formats.RecipeCollection 25 28 var rs []formats.Recipe ··· 29 32 files = unused 30 33 31 34 for _, b := range bundles { 32 - r, rc, err := f.Parse(b) 35 + r, rc, err := f.Parse(b, o) 33 36 if err != nil { 34 37 return nil, nil, err 35 38 } ··· 53 56 54 57 return rs, soloCollection, nil 55 58 } 59 + 60 + type AsType string 61 + 62 + const ( 63 + AsTypeAny AsType = "any" 64 + AsTypeRecipe AsType = "recipe" 65 + AsTypeCollection AsType = "collection" 66 + ) 67 + 68 + func ParseDestination(to string) (overrideFilename string, asType AsType, format *formats.Format) { 69 + ext := path.Ext(to) 70 + if ext != "" && ext != to { 71 + overrideFilename = to 72 + } 73 + 74 + for _, f := range AvailableFormats() { 75 + if f.Extension != "" && f.Extension == ext { 76 + return overrideFilename, AsTypeRecipe, f 77 + } 78 + if f.ExtensionCollection != "" && f.ExtensionCollection == ext { 79 + return overrideFilename, AsTypeCollection, f 80 + } 81 + if strings.EqualFold(to, f.Name) { 82 + return "", AsTypeAny, f 83 + } 84 + } 85 + 86 + return "", AsTypeAny, nil 87 + }
+51
formats_test.go
··· 1 + package recipes_test 2 + 3 + import ( 4 + "testing" 5 + 6 + . "github.com/jphastings/recipes" 7 + "github.com/jphastings/recipes/cooklang" 8 + "github.com/jphastings/recipes/crouton" 9 + "github.com/jphastings/recipes/internal/formats" 10 + "github.com/jphastings/recipes/mela" 11 + "github.com/stretchr/testify/assert" 12 + ) 13 + 14 + func TestParseDestination(t *testing.T) { 15 + tc := []struct { 16 + to string 17 + overrideFilename string 18 + asType AsType 19 + format *formats.Format 20 + }{ 21 + {"Mela", "", AsTypeAny, mela.FormatInfo}, 22 + {"mela", "", AsTypeAny, mela.FormatInfo}, 23 + {".melarecipe", "", AsTypeRecipe, mela.FormatInfo}, 24 + {".melarecipes", "", AsTypeCollection, mela.FormatInfo}, 25 + {"something.melarecipe", "something.melarecipe", AsTypeRecipe, mela.FormatInfo}, 26 + {"another.melarecipes", "another.melarecipes", AsTypeCollection, mela.FormatInfo}, 27 + 28 + {"Crouton", "", AsTypeAny, crouton.FormatInfo}, 29 + {"crouton", "", AsTypeAny, crouton.FormatInfo}, 30 + {".crumb", "", AsTypeRecipe, crouton.FormatInfo}, 31 + {"something.crumb", "something.crumb", AsTypeRecipe, crouton.FormatInfo}, 32 + 33 + {"Cooklang", "", AsTypeAny, cooklang.FormatInfo}, 34 + {"cooklang", "", AsTypeAny, cooklang.FormatInfo}, 35 + {".cook", "", AsTypeRecipe, cooklang.FormatInfo}, 36 + {"something.cook", "something.cook", AsTypeRecipe, cooklang.FormatInfo}, 37 + 38 + {"nope", "", AsTypeAny, nil}, 39 + {".nope", "", AsTypeAny, nil}, 40 + {"whatever.nope", "", AsTypeAny, nil}, 41 + } 42 + 43 + for _, c := range tc { 44 + t.Run(c.to, func(t *testing.T) { 45 + overrideFilename, asType, format := ParseDestination(c.to) 46 + assert.Equal(t, c.overrideFilename, overrideFilename) 47 + assert.Equal(t, c.asType, asType) 48 + assert.Equal(t, c.format, format) 49 + }) 50 + } 51 + }
+19 -2
go.mod
··· 6 6 7 7 require ( 8 8 github.com/SadPencil/go-lagrange-interpolation v0.0.0-20230827182818-6ce53f3fde88 9 + github.com/antchfx/htmlquery v1.3.3 9 10 github.com/gen2brain/jpegli v0.2.2 11 + github.com/pirmd/epub v0.3.0 10 12 github.com/spf13/cobra v1.8.1 13 + github.com/spf13/viper v1.19.0 11 14 github.com/stretchr/testify v1.9.0 12 15 github.com/yeka/zip v0.0.0-20231116150916-03d6312748a9 13 16 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 17 + golang.org/x/net v0.23.0 14 18 golang.org/x/text v0.16.0 15 19 ) 16 20 17 21 require ( 18 - github.com/davecgh/go-spew v1.1.1 // indirect 22 + github.com/antchfx/xpath v1.3.2 // indirect 23 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 24 + github.com/fsnotify/fsnotify v1.7.0 // indirect 25 + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 26 + github.com/hashicorp/hcl v1.0.0 // indirect 19 27 github.com/inconshreveable/mousetrap v1.1.0 // indirect 20 - github.com/pmezard/go-difflib v1.0.0 // indirect 28 + github.com/magiconair/properties v1.8.7 // indirect 29 + github.com/mitchellh/mapstructure v1.5.0 // indirect 30 + github.com/pelletier/go-toml/v2 v2.2.2 // indirect 31 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 32 + github.com/sagikazarmark/slog-shim v0.1.0 // indirect 33 + github.com/spf13/afero v1.11.0 // indirect 34 + github.com/spf13/cast v1.6.0 // indirect 21 35 github.com/spf13/pflag v1.0.5 // indirect 36 + github.com/subosito/gotenv v1.6.0 // indirect 22 37 github.com/tetratelabs/wazero v1.7.0 // indirect 23 38 golang.org/x/crypto v0.24.0 // indirect 39 + golang.org/x/sys v0.21.0 // indirect 40 + gopkg.in/ini.v1 v1.67.0 // indirect 24 41 gopkg.in/yaml.v3 v3.0.1 // indirect 25 42 )
+157
go.sum
··· 1 + cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= 2 + cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= 3 + cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= 4 + cloud.google.com/go/firestore v1.15.0/go.mod h1:GWOxFXcv8GZUtYpWHw/w6IuYNux/BtmeVTMmjrm4yhk= 5 + cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= 6 + cloud.google.com/go/longrunning v0.5.5/go.mod h1:WV2LAxD8/rg5Z1cNW6FJ/ZpX4E4VnDnoTk0yawPBB7s= 7 + cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= 1 8 github.com/SadPencil/go-lagrange-interpolation v0.0.0-20230827182818-6ce53f3fde88 h1:HYAcGO8oYL3d0tz/j1EmxRzLVuCeE06G8YOEi8UaJI0= 2 9 github.com/SadPencil/go-lagrange-interpolation v0.0.0-20230827182818-6ce53f3fde88/go.mod h1:pI4hYfnUP+ACVpp8uGZmFGUmAVEOj2hsI5dSUpqEqJM= 10 + github.com/antchfx/htmlquery v1.3.3 h1:x6tVzrRhVNfECDaVxnZi1mEGrQg3mjE/rxbH2Pe6dNE= 11 + github.com/antchfx/htmlquery v1.3.3/go.mod h1:WeU3N7/rL6mb6dCwtE30dURBnBieKDC/fR8t6X+cKjU= 12 + github.com/antchfx/xpath v1.3.2 h1:LNjzlsSjinu3bQpw9hWMY9ocB80oLOWuQqFvO6xt51U= 13 + github.com/antchfx/xpath v1.3.2/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs= 14 + github.com/antzucaro/matchr v0.0.0-20191224151129-ab6ba461ddec/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U= 15 + github.com/antzucaro/matchr v0.0.0-20210222213004-b04723ef80f0/go.mod h1:v3ZDlfVAL1OrkKHbGSFFK60k0/7hruHPDq2XMs9Gu6U= 16 + github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4= 17 + github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 18 + github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 3 19 github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 20 + github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 21 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 22 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 24 + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 25 + github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= 26 + github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 27 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 28 + github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= 29 + github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= 6 30 github.com/gen2brain/jpegli v0.2.2 h1:GrhioJ/ZqrI+N4gKMBs+sggCipBEU141MJwQKJYAmbo= 7 31 github.com/gen2brain/jpegli v0.2.2/go.mod h1:ArNook7A3NfU+ibQ5gko0DgQKilME4Q0sEt9rcSPaN8= 32 + github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 33 + github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 34 + github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 35 + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 36 + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 37 + github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 38 + github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 39 + github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= 40 + github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 41 + github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= 42 + github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= 43 + github.com/googleapis/google-cloud-go-testing v0.0.0-20210719221736-1c9a4c676720/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 44 + github.com/hashicorp/consul/api v1.28.2/go.mod h1:KyzqzgMEya+IZPcD65YFoOVAgPpbfERu4I/tzG6/ueE= 45 + github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 46 + github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 47 + github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 48 + github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= 49 + github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 50 + github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= 51 + github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 52 + github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 53 + github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 54 + github.com/hashicorp/serf v0.10.1/go.mod h1:yL2t6BqATOLGc5HF7qbFkTfXoPIY0WZdWHfEvMqbG+4= 8 55 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 9 56 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 57 + github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 58 + github.com/klauspost/compress v1.17.2/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= 59 + github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 60 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 61 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 62 + github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 63 + github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 64 + github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 65 + github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 66 + github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= 67 + github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 68 + github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 69 + github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 70 + github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 71 + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 + github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 73 + github.com/nats-io/nats.go v1.34.0/go.mod h1:Ubdu4Nh9exXdSz0RVWRFBbRfrbSxOYd26oF0wkWclB8= 74 + github.com/nats-io/nkeys v0.4.7/go.mod h1:kqXRgRDPlGy7nGaEDMuYzmiJCIAAWDK0IMBtDmGD0nc= 75 + github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= 76 + github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 77 + github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 78 + github.com/pirmd/epub v0.3.0 h1:8QOnm7tPqvdL5LMHJWPAukLm5mv0+t+HGqKkg4poND4= 79 + github.com/pirmd/epub v0.3.0/go.mod h1:AfxSPsCf8J28Tgqo8L0si2mN2ZFNB/fWkvBhNnqswZk= 80 + github.com/pirmd/text v0.6.0/go.mod h1:CK1HypnOx5CsxYOEXHiSBQxZ2skU2MECCYX5Xpv2EBk= 81 + github.com/pirmd/text v0.6.2/go.mod h1:CK1HypnOx5CsxYOEXHiSBQxZ2skU2MECCYX5Xpv2EBk= 82 + github.com/pirmd/verify v0.8.0/go.mod h1:IeD/FreSSX/rauoreHffhpKrh6Gkw9GyCX9X9kW3LxE= 83 + github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 84 + github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Qk= 10 85 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 11 86 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 87 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 88 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 89 + github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 90 + github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 91 + github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 12 92 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 93 + github.com/sagikazarmark/crypt v0.19.0/go.mod h1:c6vimRziqqERhtSe0MhIvzE1w54FrCHtrXb5NH/ja78= 94 + github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= 95 + github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= 96 + github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= 97 + github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 98 + github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= 99 + github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= 100 + github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 101 + github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 13 102 github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= 14 103 github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 15 104 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 16 105 github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 106 + github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= 107 + github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= 108 + github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 109 + github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 110 + github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 17 111 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 112 + github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 113 + github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 114 + github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 18 115 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 19 116 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 117 + github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= 118 + github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= 20 119 github.com/tetratelabs/wazero v1.7.0 h1:jg5qPydno59wqjpGrHph81lbtHzTrWzwwtD4cD88+hQ= 21 120 github.com/tetratelabs/wazero v1.7.0/go.mod h1:ytl6Zuh20R/eROuyDaGPkp82O9C/DJfXAwJfQ3X6/7Y= 22 121 github.com/yeka/zip v0.0.0-20231116150916-03d6312748a9 h1:K8gF0eekWPEX+57l30ixxzGhHH/qscI3JCnuhbN6V4M= 23 122 github.com/yeka/zip v0.0.0-20231116150916-03d6312748a9/go.mod h1:9BnoKCcgJ/+SLhfAXj15352hTOuVmG5Gzo8xNRINfqI= 123 + github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 124 + go.etcd.io/etcd/api/v3 v3.5.12/go.mod h1:Ot+o0SWSyT6uHhA56al1oCED0JImsRiU9Dc26+C2a+4= 125 + go.etcd.io/etcd/client/pkg/v3 v3.5.12/go.mod h1:seTzl2d9APP8R5Y2hFL3NVlD6qC/dOT+3kvrqPyTas4= 126 + go.etcd.io/etcd/client/v2 v2.305.12/go.mod h1:aQ/yhsxMu+Oht1FOupSr60oBvcS9cKXHrzBpDsPTf9E= 127 + go.etcd.io/etcd/client/v3 v3.5.12/go.mod h1:tSbBCakoWmmddL+BKVAJHa9km+O/E+bumDe9mSbPiqw= 128 + go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 129 + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= 130 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= 131 + go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= 132 + go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= 133 + go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= 134 + go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 135 + go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= 136 + go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= 137 + golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 138 + golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 24 139 golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= 25 140 golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= 141 + golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= 26 142 golang.org/x/image v0.0.0-20211028202545-6944b10bf410 h1:hTftEOvwiOq2+O8k2D5/Q7COC7k5Qcrgc2TFURJYnvQ= 27 143 golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= 144 + golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 28 145 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 146 + golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 147 + golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 148 + golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 149 + golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 150 + golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 29 151 golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 152 + golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= 153 + golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= 154 + golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= 155 + golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 156 + golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 157 golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 158 + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 159 + golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 160 + golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 161 + golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 162 + golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 163 + golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 164 + golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= 31 165 golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 166 + golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 167 + golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 168 + golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 169 + golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 32 170 golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 171 + golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 172 + golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 173 golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 174 + golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 175 + golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 34 176 golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 35 177 golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 178 + golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 36 179 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 180 + golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 181 + golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 37 182 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= 183 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 184 + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= 185 + google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o= 186 + google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 187 + google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= 188 + google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= 189 + google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= 190 + google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= 191 + google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 38 192 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 193 + gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 194 + gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 195 + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 39 196 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 197 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+2 -2
internal/formats/interfaces.go
··· 11 11 // The name of the recipe 12 12 Name() string 13 13 // The format this recipe is in 14 - Format() Format 14 + Format() *Format 15 15 // Returns the stored filename (with extension), or an appropriate generated one 16 16 Filename() string 17 17 // Destructively applies all standardizations to this recipe, returning ··· 31 31 // The name of the recipe collection 32 32 Name() string 33 33 // The format this recipe collection is in 34 - Format() Format 34 + Format() *Format 35 35 // Returns the stored filename (with extension), or an appropriate generated one 36 36 Filename() string 37 37 // Adds the provided recipe to this collection
+30
internal/formats/options.go
··· 1 + package formats 2 + 3 + import ( 4 + "fmt" 5 + 6 + "github.com/jphastings/recipes/internal/llm" 7 + "github.com/spf13/viper" 8 + ) 9 + 10 + func LoadOptions() (ParseOptions, error) { 11 + viper.SetConfigName("config") 12 + viper.AddConfigPath("$HOME/.recipes") 13 + viper.SetEnvPrefix("RECIPE") 14 + viper.AutomaticEnv() 15 + 16 + if err := viper.ReadInConfig(); err != nil { 17 + return ParseOptions{}, fmt.Errorf("unable to read config: %w", err) 18 + } 19 + 20 + o := ParseOptions{} 21 + 22 + if url := viper.GetString("LLM_URL"); url != "" { 23 + c, err := llm.NewLMStudioConnection(url, viper.GetString("LLM_MODEL")) 24 + if err == nil { 25 + o.LLM = c 26 + } 27 + } 28 + 29 + return o, nil 30 + }
+50
internal/formats/recipe.go
··· 1 + package formats 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "time" 7 + 8 + "github.com/jphastings/recipes/internal/standardize" 9 + ) 10 + 11 + var _ Recipe = (*InterchangeRecipe)(nil) 12 + 13 + // A generic and internal structure for recipes that is used for conversion 14 + // ⚠️ This struct is highly likely to change subtly with each new recipe format added to this library. 15 + type InterchangeRecipe struct { 16 + filename string 17 + ID string `json:"-"` 18 + Title string `json:"title"` 19 + Description string `json:"description"` 20 + Yield string `json:"yield"` 21 + Ingredients []IngredientGroup `json:"ingredients"` 22 + Instructions []InstructionGroup `json:"instructions"` 23 + 24 + // TODO: Handle these 25 + PrepTime time.Duration `json:"-"` 26 + CookTime time.Duration `json:"-"` 27 + TotalTime time.Duration `json:"-"` 28 + } 29 + 30 + type IngredientGroup struct { 31 + GroupName string `json:"groupName"` 32 + Ingredients []string `json:"ingredients"` 33 + } 34 + 35 + type InstructionGroup struct { 36 + GroupName string `json:"groupName"` 37 + Steps []string `json:"steps"` 38 + } 39 + 40 + func (ir InterchangeRecipe) Filename() string { return ir.filename } 41 + func (ir InterchangeRecipe) Format() *Format { return nil } 42 + func (ir InterchangeRecipe) Export() (InterchangeRecipe, error) { return ir, nil } 43 + func (ir InterchangeRecipe) Name() string { return ir.Title } 44 + func (ir InterchangeRecipe) Standardize() ([]standardize.Std, error) { 45 + return nil, fmt.Errorf("standardising a recipe in the interchange format is not supported yet") 46 + } 47 + 48 + func (ir InterchangeRecipe) Marshal(io.Writer) error { 49 + return fmt.Errorf("writing a recipe in the interchange format is not supported") 50 + }
+10
internal/formats/recipes.go
··· 1 + package formats 2 + 3 + import ( 4 + _ "embed" 5 + 6 + "github.com/jphastings/recipes/internal/llm" 7 + ) 8 + 9 + //go:embed recipes.schema.json 10 + var RecipesSchema llm.RawJSON
+92
internal/formats/recipes.schema.json
··· 1 + { 2 + "description": "A list of the recipes being converted.", 3 + "type": "array", 4 + "items": { 5 + "type": "object", 6 + "properties": { 7 + "title": { 8 + "type": "string", 9 + "description": "Title of the recipe" 10 + }, 11 + "description": { 12 + "type": "string", 13 + "description": "A short description of the recipe which is displayed after the title. May include links to other recipes in markdown format." 14 + }, 15 + "photos": { 16 + "type": "array", 17 + "items": { 18 + "type": "string" 19 + }, 20 + "description": "List of the paths to photos associated with this recipe" 21 + }, 22 + "yield": { 23 + "type": "string", 24 + "description": "The recipe's yield or servings including the unit if known" 25 + }, 26 + "prepTime": { 27 + "type": "string", 28 + "description": "How long it takes to prepare this recipe, eg. 3h30m" 29 + }, 30 + "cookTime": { 31 + "type": "string", 32 + "description": "How long it takes to cook this recipe, eg. 1h15m" 33 + }, 34 + "totalTime": { 35 + "type": "string", 36 + "description": "Total time it takes to prepare and cook the dish. This does not have to be the sum of prepTime and cookTime (but mostly is). eg. 4h45m" 37 + }, 38 + "ingredients": { 39 + "type": "array", 40 + "items": { 41 + "type": "object", 42 + "properties": { 43 + "groupName": { 44 + "type": "string", 45 + "description": "The name of the part of the recipe this group of ingredients makes. Empty if there's only one group" 46 + }, 47 + "ingredients": { 48 + "type": "array", 49 + "items": { 50 + "type": "string" 51 + }, 52 + "description": "A list of ingredients, each one's quantity, unit and name" 53 + } 54 + } 55 + }, 56 + "description": "The ingredients for the recipes, broken down into groups if needed" 57 + }, 58 + "instructions": { 59 + "type": "array", 60 + "items": { 61 + "type": "object", 62 + "properties": { 63 + "groupName": { 64 + "type": "string", 65 + "description": "The name of the part of the recipe this set of instructions is for. Empty if there's only one group" 66 + }, 67 + "steps": { 68 + "type": "array", 69 + "items": { 70 + "type": "string", 71 + "description": "A single instruction step for making the recipe, without preceding bullet point or number." 72 + } 73 + } 74 + } 75 + }, 76 + "description": "The instructions for making the recipe" 77 + }, 78 + "notes": { 79 + "type": "string", 80 + "description": "Any additional notes about the recipe that aren't a part of the description" 81 + } 82 + }, 83 + "required": [ 84 + "title", 85 + "description", 86 + "yield", 87 + "ingredients", 88 + "instructions" 89 + ], 90 + "additionalProperties": false 91 + } 92 + }
+6 -18
internal/formats/types.go
··· 1 1 package formats 2 2 3 - import ( 4 - "time" 5 - ) 3 + import "github.com/jphastings/recipes/internal/llm" 6 4 7 5 // Details about a recipe format 8 6 type Format struct { ··· 17 15 // The file extension for the collection format (with period) 18 16 ExtensionCollection string 19 17 // Turns one interchange recipe format into this format 20 - New func(InterchangeRecipe) (Recipe, error) 18 + New func(Recipe) (Recipe, error) 21 19 // Turns one or more interchange recipes into this collection format 22 20 // Will be nil if this is not a collection format 23 - NewCollection func(string, []InterchangeRecipe) (RecipeCollection, error) 21 + NewCollection func(name string, recipes []Recipe) (RecipeCollection, error) 24 22 // Parses a filesystem object into either a single Recipe *or* a single RecipeCollection. 25 - // A response of nil, nil, nil means the file is definitively not of this format. 26 - Parse func(Bundle) (Recipe, RecipeCollection, error) 23 + Parse func(Bundle, ParseOptions) (Recipe, RecipeCollection, error) 27 24 // Bundle must extract sets of recipe files for this format that *must* be processed together. 28 25 // Eg. cooklang stores images adjacent to the recipe file: 29 26 // lasagne.cook, lasagne.jpg, shakshouka.cook, random.jpg, ignored.crumb ··· 51 48 return out 52 49 } 53 50 54 - // A generic and internal structure for recipes that is used for conversion 55 - // ⚠️ This struct is highly likely to change subtly with each new recipe format added to this library. 56 - type InterchangeRecipe struct { 57 - Filename string 58 - ID string 59 - Title string 60 - Description string 61 - 62 - PrepTime time.Duration 63 - CookTime time.Duration 64 - TotalTime time.Duration 51 + type ParseOptions struct { 52 + LLM *llm.Connection 65 53 }
+114
internal/llm/llm.go
··· 1 + package llm 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "fmt" 7 + "io" 8 + "net/http" 9 + ) 10 + 11 + type Connection struct { 12 + Endpoint string 13 + Model string 14 + APIKey string 15 + } 16 + 17 + func NewLMStudioConnection(endpoint, model string) (*Connection, error) { 18 + conn := &Connection{ 19 + Endpoint: endpoint, 20 + Model: model, 21 + } 22 + 23 + return conn, nil 24 + } 25 + 26 + type request struct { 27 + Model string `json:"model"` 28 + Messages []Message `json:"messages"` 29 + ResponseFormat ReqResponseFormat `json:"response_format"` 30 + Temperature float32 `json:"temperature"` 31 + Stream bool `json:"stream"` 32 + } 33 + 34 + type response struct { 35 + Choices []struct { 36 + Message struct { 37 + Content string `json:"content"` 38 + } `json:"message"` 39 + } `json:"choices"` 40 + } 41 + 42 + type Message struct { 43 + Role string `json:"role"` 44 + Content string `json:"content"` 45 + } 46 + 47 + type ReqResponseFormat struct { 48 + Type string `json:"type"` 49 + JSONSchema JSONSchema `json:"json_schema"` 50 + } 51 + 52 + type JSONSchema struct { 53 + Name string `json:"name"` 54 + Strict bool `json:"strict"` 55 + Schema RawJSON `json:"schema"` 56 + } 57 + 58 + type RawJSON string 59 + 60 + func (j RawJSON) MarshalJSON() ([]byte, error) { 61 + return []byte(j), nil 62 + } 63 + 64 + func (c *Connection) StructuredQuery(systemPrompt, userPrompt string, schema RawJSON, target any) error { 65 + req := request{ 66 + Model: c.Model, 67 + Messages: []Message{ 68 + {Role: "system", Content: systemPrompt}, 69 + {Role: "user", Content: userPrompt}, 70 + }, 71 + ResponseFormat: ReqResponseFormat{ 72 + Type: "json_schema", 73 + JSONSchema: JSONSchema{ 74 + // Name: "mela_recipe", 75 + Strict: true, 76 + Schema: RawJSON(schema), 77 + }, 78 + }, 79 + Temperature: 0.7, 80 + Stream: false, 81 + } 82 + 83 + reqEnc, err := json.Marshal(req) 84 + if err != nil { 85 + return err 86 + } 87 + 88 + res, err := http.Post(c.Endpoint, "application/json", bytes.NewReader(reqEnc)) 89 + if err != nil { 90 + return err 91 + } 92 + 93 + data, err := io.ReadAll(res.Body) 94 + if err != nil { 95 + return err 96 + } 97 + 98 + var answer response 99 + if err := json.Unmarshal(data, &answer); err != nil { 100 + return fmt.Errorf("couldn't decode JSON response: %w", err) 101 + } 102 + 103 + if len(answer.Choices) == 0 { 104 + return fmt.Errorf("the LLM returned zero response messages") 105 + } 106 + 107 + jsonText := answer.Choices[0].Message.Content 108 + 109 + if err := json.Unmarshal([]byte(jsonText), target); err != nil { 110 + return fmt.Errorf("couldn't decode JSON into provided target: %w", err) 111 + } 112 + 113 + return nil 114 + }
+6 -5
mela/format.go
··· 1 1 package mela 2 2 3 3 import ( 4 + "fmt" 4 5 "os" 5 6 "path" 6 7 ··· 12 13 collectionExt = ".melarecipes" 13 14 ) 14 15 15 - var FormatInfo = formats.Format{ 16 + var FormatInfo = &formats.Format{ 16 17 Name: "Mela", 17 18 URL: "https://mela.recipes", 18 19 Features: formats.Features{ ··· 23 24 }, 24 25 Extension: recipeExt, 25 26 ExtensionCollection: collectionExt, 26 - New: newFromInterchange, 27 - NewCollection: newFromInterchangeCollection, 27 + New: importRecipe, 28 + NewCollection: importCollection, 28 29 Parse: Parse, 29 30 Bundle: formats.BundleByExtension(recipeExt, collectionExt), 30 31 } 31 32 32 33 const ZipFileMagicBytes = "PK\x03\x04" 33 34 34 - func Parse(b formats.Bundle) (formats.Recipe, formats.RecipeCollection, error) { 35 + func Parse(b formats.Bundle, _ formats.ParseOptions) (formats.Recipe, formats.RecipeCollection, error) { 35 36 filename := b[0] 36 37 ext := path.Ext(filename) 37 38 if ext != recipeExt && ext != collectionExt { 38 - return nil, nil, nil 39 + return nil, nil, fmt.Errorf("doesn't appear to be a Mela recipe or collection file") 39 40 } 40 41 41 42 f, err := os.Open(filename)
+41 -6
mela/import.go
··· 4 4 "github.com/jphastings/recipes/internal/formats" 5 5 ) 6 6 7 - func newFromInterchange(ir formats.InterchangeRecipe) (formats.Recipe, error) { 7 + func importRecipe(r formats.Recipe) (formats.Recipe, error) { 8 + if _, ok := r.(*Recipe); ok { 9 + return r, nil 10 + } 11 + 12 + ir, err := r.Export() 13 + if err != nil { 14 + return nil, err 15 + } 16 + 8 17 return &Recipe{ 9 - filename: ir.Filename, 18 + filename: r.Filename(), 10 19 ID: ir.ID, 11 20 Title: ir.Title, 12 - Text: ir.Description, 13 - // TODO: Other fields 21 + // TODO: Link 22 + Text: ir.Description, 23 + Ingredients: ingredientsToSecSeq(ir.Ingredients), 24 + Instructions: instructionsToSecSeq(ir.Instructions), 25 + // TODO: Images 26 + Yield: PeopleCount(ir.Yield), 27 + 28 + PrepTime: MaybeDuration(ir.PrepTime.String()), 29 + CookTime: MaybeDuration(ir.CookTime.String()), 30 + TotalTime: MaybeDuration(ir.TotalTime.String()), 14 31 }, nil 15 32 } 16 33 17 - func newFromInterchangeCollection(name string, rs []formats.InterchangeRecipe) (formats.RecipeCollection, error) { 34 + func ingredientsToSecSeq(igs []formats.IngredientGroup) SectionedSequence { 35 + m := make(map[string][]string) 36 + for _, ig := range igs { 37 + m[ig.GroupName] = ig.Ingredients 38 + } 39 + 40 + return NewSectionedSequence(m) 41 + } 42 + 43 + func instructionsToSecSeq(igs []formats.InstructionGroup) SectionedSequence { 44 + m := make(map[string][]string) 45 + for _, ig := range igs { 46 + m[ig.GroupName] = ig.Steps 47 + } 48 + 49 + return NewSectionedSequence(m) 50 + } 51 + 52 + func importCollection(name string, rs []formats.Recipe) (formats.RecipeCollection, error) { 18 53 rc := &RecipeCollection{ 19 54 name: name, 20 55 recipes: make([]*Recipe, len(rs)), 21 56 } 22 57 23 58 for i, ir := range rs { 24 - r, err := newFromInterchange(ir) 59 + r, err := importRecipe(ir) 25 60 if err != nil { 26 61 return rc, err 27 62 }
+3 -3
mela/recipe.go
··· 33 33 TotalTime MaybeDuration `json:"totalTime"` 34 34 } 35 35 36 - func (r Recipe) Name() string { return r.Title } 37 - func (r Recipe) Format() formats.Format { return FormatInfo } 38 - func (r Recipe) Filename() string { return r.filename + FormatInfo.Extension } 36 + func (r Recipe) Name() string { return r.Title } 37 + func (r Recipe) Format() *formats.Format { return FormatInfo } 38 + func (r Recipe) Filename() string { return r.filename + FormatInfo.Extension } 39 39 40 40 var ErrInvalidMelaFile = errors.New("given file is neither a melarecipe nor a melarecipes file") 41 41 var ErrInvalidMelaRecipeFile = errors.New("given file is not a melarecipe file")
+5 -5
mela/recipes.go
··· 49 49 return rs, nil 50 50 } 51 51 52 - func (rc *RecipeCollection) Filename() string { return rc.filename + FormatInfo.ExtensionCollection } 53 - func (rc *RecipeCollection) Format() formats.Format { return FormatInfo } 52 + func (rc *RecipeCollection) Filename() string { return rc.filename + FormatInfo.ExtensionCollection } 53 + func (rc *RecipeCollection) Format() *formats.Format { return FormatInfo } 54 54 55 55 func (rc *RecipeCollection) Add(rs ...formats.Recipe) error { 56 56 for _, ir := range rs { ··· 65 65 return nil 66 66 } 67 67 68 - func (rs *RecipeCollection) Recipes() []formats.Recipe { 69 - out := make([]formats.Recipe, len(rs.recipes)) 70 - for i, r := range rs.recipes { 68 + func (rc *RecipeCollection) Recipes() []formats.Recipe { 69 + out := make([]formats.Recipe, len(rc.recipes)) 70 + for i, r := range rc.recipes { 71 71 out[i] = formats.Recipe(r) 72 72 } 73 73 return out
+5 -1
utils/pages.go
··· 4 4 "fmt" 5 5 "math" 6 6 "net/url" 7 + "regexp" 7 8 "strconv" 8 9 "strings" 9 10 ) 10 11 11 12 type Pages []PageRange 12 13 type PageRange []string 14 + 15 + var pageSplit = regexp.MustCompile(`[-–—]`) 13 16 14 17 func ParsePages(pages string) (Pages, error) { 15 18 var pageRanges []PageRange 16 19 parts := strings.Split(pages, ",") 17 20 for _, prs := range parts { 18 - ps := strings.Split(prs, "-") 21 + ps := pageSplit.Split(prs, -1) 19 22 if len(ps) > 2 { 20 23 return nil, fmt.Errorf("invalid page range: %s", prs) 21 24 } ··· 28 31 if pr[i], err = url.QueryUnescape(p); err != nil { 29 32 return nil, fmt.Errorf("invalid URL encoding: %s", p) 30 33 } 34 + pr[i] = strings.TrimSpace(pr[i]) 31 35 } 32 36 pageRanges = append(pageRanges, pr) 33 37 }
+18
utils/progress.go
··· 1 + package utils 2 + 3 + type progress struct { 4 + total int 5 + channel chan float32 6 + } 7 + 8 + func NewProgress(total int) (progress, <-chan float32) { 9 + pt := progress{ 10 + total: total, 11 + channel: make(chan float32), 12 + } 13 + return pt, pt.channel 14 + } 15 + 16 + func (pt progress) Update(current int) { 17 + pt.channel <- float32(current) / float32(pt.total) 18 + }