From 28b18ec679c5a11721cffa2b003ca48c0c6fbee6 Mon Sep 17 00:00:00 2001 From: Tony Blyler Date: Sat, 8 Jul 2017 01:17:01 -0400 Subject: [PATCH] Initial dirty but working commit --- README.md | 13 + doc/docx.go | 131 ++++++++++ doc/docx_test.go | 610 +++++++++++++++++++++++++++++++++++++++++++++++ handler.go | 534 +++++++++++++++++++++++++++++++++++++++++ main.go | 144 +++++++++++ recipe/recipe.go | 186 +++++++++++++++ templates.go | 309 ++++++++++++++++++++++++ 7 files changed, 1927 insertions(+) create mode 100644 README.md create mode 100644 doc/docx.go create mode 100644 doc/docx_test.go create mode 100644 handler.go create mode 100644 main.go create mode 100644 recipe/recipe.go create mode 100644 templates.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..712e169 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# recipe-card +WIP (but fully functional) and extremely messy at the moment. + +## Quick Start +Use the Google Docs recipe template to for your recipes. + +Have scans of jpegs by your docx files, and have your docx/images in folders specific to that recipe. + +One docx file and as many jpeg images as you want. The image in the recipe tempalte will also be used. + +Just `go get github.com/tblyler/recipe-card` and run `recipe-card`. + +Run with `--help` for options. diff --git a/doc/docx.go b/doc/docx.go new file mode 100644 index 0000000..4c1f983 --- /dev/null +++ b/doc/docx.go @@ -0,0 +1,131 @@ +package doc + +import ( + "archive/zip" + "bytes" + "encoding/xml" + "fmt" + "io" + "io/ioutil" + "strings" +) + +const ( + // xmlFileName is the one true XML file in a docx file that has + // the textual information we desire + xmlFileName = "word/document.xml" +) + +var ( + // ErrMissingDocument happens when xmlFileName is missing from zip + ErrMissingDocument = fmt.Errorf("Unable to find %s in docx", xmlFileName) +) + +// Docx parses docx-formated readers +// this is go routine safe +type Docx struct { + xmlData []byte + Image []byte +} + +// NewDocx creates a new Docx instance with data from the given reader +func NewDocx(reader io.ReaderAt, size int64) (doc *Docx, err error) { + doc = new(Docx) + + // docx files are just zip'd xml documents + zipReader, err := zip.NewReader(reader, size) + if err != nil { + return + } + + // find the xmlFileName file in the zip + var fileReader io.ReadCloser + for _, file := range zipReader.File { + if doc.xmlData != nil && doc.Image != nil { + return + } + + lowerFileName := strings.ToLower(file.Name) + if doc.Image == nil && (strings.HasSuffix(lowerFileName, ".jpg") || strings.HasSuffix(lowerFileName, ".jpeg")) { + fileReader, err = file.Open() + if err != nil { + continue + } + + defer fileReader.Close() + + doc.Image, err = ioutil.ReadAll(fileReader) + if err != nil { + return + } + } else if doc.xmlData == nil && lowerFileName == xmlFileName { + // open xmlFileName for extraction + fileReader, err = file.Open() + if err != nil { + return + } + + defer fileReader.Close() + + // store all extracted XML data to doc.xmlData + doc.xmlData, err = ioutil.ReadAll(fileReader) + if err != nil { + return + } + } + } + + if doc.xmlData != nil && doc.Image != nil { + return + } + + return nil, ErrMissingDocument +} + +// Text returns each line of (unformatted) text from the docx xml +func (d *Docx) Text() (lines []string, err error) { + // create an XML decoder for the raw xml data + decoder := xml.NewDecoder(bytes.NewReader(d.xmlData)) + + // determines if xml.CharData tokens should start to be added to the + // lines slice + outputCharData := false + + var token xml.Token + for { + // get the current xml token + token, err = decoder.Token() + if err != nil { + // end of file reached, reset err to nil + if err == io.EOF { + err = nil + } + + return + } + + switch t := token.(type) { + case xml.StartElement: + // only start outputing chardata xml tokens if we started to look at + // the "body" of the xml document + if !outputCharData && strings.ToLower(t.Name.Local) == "body" { + outputCharData = true + } + + break + + case xml.CharData: + if outputCharData { + // cast to string and get rid of unneeded whitespace + str := strings.TrimSpace(string(t)) + + // only add lines that actually have data + if str != "" { + lines = append(lines, str) + } + } + + break + } + } +} diff --git a/doc/docx_test.go b/doc/docx_test.go new file mode 100644 index 0000000..165d435 --- /dev/null +++ b/doc/docx_test.go @@ -0,0 +1,610 @@ +package doc + +import ( + "archive/zip" + "bytes" + "crypto/rand" + "io" + "testing" +) + +var testDocx = []byte{80, 75, 3, 4, 20, 0, 8, 8, 8, 0, 7, 140, + 222, 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 11, 0, 0, 0, 95, 114, 101, 108, 115, 47, + 46, 114, 101, 108, 115, 173, 146, 77, 75, 3, 65, 12, + 134, 239, 253, 21, 67, 238, 221, 108, 43, 136, 200, 206, + 246, 34, 66, 111, 34, 245, 7, 132, 153, 236, 238, 208, + 206, 7, 51, 105, 173, 255, 222, 65, 10, 186, 80, 138, + 160, 199, 188, 121, 243, 240, 28, 210, 109, 206, 254, 160, + 78, 156, 139, 139, 65, 195, 170, 105, 65, 113, 48, 209, + 186, 48, 106, 120, 219, 61, 47, 31, 96, 211, 47, 186, + 87, 62, 144, 212, 74, 153, 92, 42, 170, 222, 132, 162, + 97, 18, 73, 143, 136, 197, 76, 236, 169, 52, 49, 113, + 168, 155, 33, 102, 79, 82, 199, 60, 98, 34, 179, 167, + 145, 113, 221, 182, 247, 152, 127, 50, 160, 159, 49, 213, + 214, 106, 200, 91, 187, 2, 181, 251, 72, 252, 55, 54, + 122, 22, 178, 36, 132, 38, 102, 94, 166, 92, 175, 179, + 56, 46, 21, 78, 121, 100, 209, 96, 163, 121, 169, 113, + 249, 106, 52, 149, 12, 120, 93, 104, 253, 123, 161, 56, + 12, 206, 240, 83, 52, 71, 207, 65, 174, 121, 241, 89, + 56, 88, 182, 183, 149, 40, 165, 91, 70, 119, 255, 105, + 52, 111, 124, 203, 188, 199, 108, 209, 94, 226, 139, 205, + 162, 195, 217, 27, 244, 159, 80, 75, 7, 8, 232, 208, + 1, 35, 217, 0, 0, 0, 61, 2, 0, 0, 80, 75, + 3, 4, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 16, 0, 0, 0, 100, 111, 99, 80, 114, 111, 112, 115, + 47, 97, 112, 112, 46, 120, 109, 108, 157, 145, 203, 110, + 194, 48, 16, 69, 247, 253, 138, 200, 98, 75, 156, 240, + 42, 69, 142, 81, 31, 234, 10, 169, 72, 77, 75, 119, + 200, 181, 39, 137, 171, 196, 182, 236, 1, 193, 223, 215, + 20, 41, 100, 221, 221, 189, 115, 71, 103, 198, 99, 182, + 62, 117, 109, 114, 4, 31, 180, 53, 5, 201, 211, 140, + 36, 96, 164, 85, 218, 212, 5, 249, 40, 95, 199, 75, + 146, 4, 20, 70, 137, 214, 26, 40, 200, 25, 2, 89, + 243, 59, 182, 245, 214, 129, 71, 13, 33, 137, 4, 19, + 10, 210, 32, 186, 21, 165, 65, 54, 208, 137, 144, 198, + 216, 196, 164, 178, 190, 19, 24, 173, 175, 169, 173, 42, + 45, 225, 197, 202, 67, 7, 6, 233, 36, 203, 22, 20, + 78, 8, 70, 129, 26, 187, 30, 72, 174, 196, 213, 17, + 255, 11, 85, 86, 94, 246, 11, 159, 229, 217, 69, 30, + 103, 37, 116, 174, 21, 8, 156, 209, 155, 44, 45, 138, + 182, 212, 29, 240, 44, 150, 123, 195, 30, 157, 107, 181, + 20, 24, 47, 194, 55, 250, 219, 195, 219, 223, 8, 58, + 79, 167, 233, 44, 205, 71, 27, 109, 14, 167, 253, 215, + 114, 177, 95, 204, 146, 65, 195, 62, 62, 225, 7, 36, + 210, 105, 54, 122, 58, 232, 86, 141, 115, 70, 135, 48, + 182, 21, 53, 4, 30, 171, 87, 193, 118, 214, 171, 192, + 39, 115, 70, 175, 138, 61, 55, 194, 11, 137, 241, 59, + 248, 195, 148, 209, 129, 29, 68, 59, 141, 205, 187, 19, + 242, 130, 202, 150, 195, 174, 65, 18, 103, 121, 81, 123, + 225, 154, 192, 239, 47, 3, 123, 23, 77, 127, 106, 254, + 11, 80, 75, 7, 8, 126, 47, 253, 199, 37, 1, 0, + 0, 0, 2, 0, 0, 80, 75, 3, 4, 20, 0, 8, + 8, 8, 0, 7, 140, 222, 74, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, 0, 100, + 111, 99, 80, 114, 111, 112, 115, 47, 99, 111, 114, 101, + 46, 120, 109, 108, 109, 82, 91, 79, 194, 48, 20, 126, + 247, 87, 44, 125, 223, 186, 139, 65, 211, 108, 35, 81, + 195, 147, 36, 38, 64, 52, 190, 213, 238, 48, 170, 107, + 215, 180, 7, 6, 255, 222, 110, 192, 132, 200, 219, 249, + 46, 253, 78, 207, 105, 243, 233, 94, 53, 193, 14, 172, + 147, 173, 46, 72, 18, 197, 36, 0, 45, 218, 74, 234, + 186, 32, 171, 229, 44, 124, 36, 129, 67, 174, 43, 222, + 180, 26, 10, 114, 0, 71, 166, 229, 93, 46, 12, 19, + 173, 133, 55, 219, 26, 176, 40, 193, 5, 62, 72, 59, + 38, 76, 65, 54, 136, 134, 81, 234, 196, 6, 20, 119, + 145, 119, 104, 47, 174, 91, 171, 56, 122, 104, 107, 106, + 184, 248, 225, 53, 208, 52, 142, 39, 84, 1, 242, 138, + 35, 167, 125, 96, 104, 198, 68, 114, 138, 172, 196, 24, + 105, 182, 182, 25, 2, 42, 65, 161, 1, 5, 26, 29, + 77, 162, 132, 254, 121, 17, 172, 114, 55, 15, 12, 202, + 133, 83, 73, 60, 24, 184, 105, 61, 139, 163, 123, 239, + 228, 104, 236, 186, 46, 234, 178, 193, 234, 239, 159, 208, + 143, 249, 235, 98, 24, 53, 148, 186, 95, 149, 0, 82, + 230, 149, 96, 194, 2, 199, 214, 150, 57, 189, 4, 190, + 174, 192, 9, 43, 13, 250, 149, 31, 197, 43, 194, 227, + 134, 235, 122, 235, 247, 83, 130, 14, 87, 139, 193, 50, + 82, 253, 230, 27, 238, 112, 238, 223, 104, 45, 161, 122, + 58, 248, 140, 27, 220, 105, 19, 76, 157, 184, 192, 143, + 192, 142, 3, 159, 165, 247, 236, 249, 101, 57, 35, 101, + 26, 39, 15, 97, 60, 9, 179, 120, 153, 100, 44, 75, + 89, 114, 255, 217, 55, 189, 14, 24, 58, 91, 216, 201, + 254, 171, 148, 233, 208, 116, 132, 253, 173, 221, 246, 235, + 27, 4, 30, 71, 26, 129, 175, 81, 98, 3, 71, 250, + 92, 254, 251, 62, 229, 47, 80, 75, 7, 8, 45, 164, + 180, 116, 82, 1, 0, 0, 138, 2, 0, 0, 80, 75, + 3, 4, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 28, 0, 0, 0, 119, 111, 114, 100, 47, 95, 114, 101, + 108, 115, 47, 100, 111, 99, 117, 109, 101, 110, 116, 46, + 120, 109, 108, 46, 114, 101, 108, 115, 173, 146, 203, 106, + 195, 48, 16, 69, 247, 249, 10, 49, 251, 90, 118, 250, + 160, 20, 203, 217, 148, 66, 182, 197, 253, 0, 69, 30, + 63, 136, 245, 64, 154, 148, 230, 239, 59, 52, 33, 113, + 32, 152, 46, 188, 188, 87, 154, 59, 71, 51, 42, 55, + 63, 118, 20, 223, 24, 211, 224, 157, 130, 34, 203, 65, + 160, 51, 190, 25, 92, 167, 224, 171, 254, 120, 120, 133, + 77, 181, 42, 63, 113, 212, 196, 87, 82, 63, 132, 36, + 184, 198, 37, 5, 61, 81, 120, 147, 50, 153, 30, 173, + 78, 153, 15, 232, 248, 164, 245, 209, 106, 98, 25, 59, + 25, 180, 217, 235, 14, 229, 58, 207, 95, 100, 156, 102, + 64, 117, 147, 41, 182, 141, 130, 184, 109, 10, 16, 245, + 49, 224, 127, 178, 125, 219, 14, 6, 223, 189, 57, 88, + 116, 116, 167, 133, 76, 116, 28, 49, 113, 162, 142, 29, + 146, 130, 147, 206, 56, 7, 228, 253, 246, 235, 37, 219, + 187, 131, 221, 97, 228, 65, 94, 9, 46, 214, 28, 196, + 227, 146, 16, 173, 119, 84, 235, 221, 136, 87, 136, 139, + 53, 7, 241, 180, 232, 34, 144, 136, 31, 61, 93, 197, + 217, 153, 67, 120, 94, 18, 129, 184, 118, 50, 131, 63, + 121, 50, 139, 51, 195, 170, 148, 55, 191, 188, 250, 5, + 80, 75, 7, 8, 41, 11, 4, 96, 232, 0, 0, 0, + 28, 3, 0, 0, 80, 75, 3, 4, 20, 0, 8, 8, + 8, 0, 7, 140, 222, 74, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 119, 111, + 114, 100, 47, 115, 116, 121, 108, 101, 115, 46, 120, 109, + 108, 205, 87, 93, 83, 234, 48, 16, 125, 191, 191, 162, + 147, 119, 44, 32, 42, 50, 86, 71, 113, 28, 153, 97, + 240, 142, 232, 15, 88, 210, 45, 228, 154, 38, 189, 73, + 42, 226, 175, 191, 73, 105, 17, 108, 169, 223, 195, 229, + 1, 154, 179, 205, 246, 236, 57, 219, 217, 112, 114, 246, + 20, 115, 239, 17, 149, 102, 82, 4, 164, 181, 215, 36, + 30, 10, 42, 67, 38, 166, 1, 185, 191, 187, 106, 116, + 137, 167, 13, 136, 16, 184, 20, 24, 144, 5, 106, 114, + 118, 250, 235, 100, 222, 211, 102, 193, 81, 123, 118, 191, + 208, 189, 121, 64, 102, 198, 36, 61, 223, 215, 116, 134, + 49, 232, 61, 153, 160, 176, 177, 72, 170, 24, 140, 93, + 170, 169, 63, 151, 42, 76, 148, 164, 168, 181, 77, 31, + 115, 191, 221, 108, 30, 250, 49, 48, 65, 138, 52, 173, + 78, 41, 81, 204, 168, 146, 90, 70, 102, 143, 202, 216, + 151, 81, 196, 40, 102, 169, 236, 246, 86, 51, 187, 138, + 121, 145, 32, 166, 239, 33, 18, 131, 122, 72, 147, 134, + 205, 151, 128, 97, 19, 198, 153, 89, 100, 100, 136, 23, + 211, 222, 96, 42, 164, 130, 9, 183, 213, 90, 62, 228, + 212, 214, 26, 74, 122, 137, 17, 164, 220, 104, 183, 84, + 191, 85, 190, 204, 87, 217, 207, 149, 20, 70, 123, 243, + 30, 104, 202, 88, 64, 134, 108, 130, 202, 166, 151, 194, + 27, 163, 98, 17, 177, 161, 217, 185, 208, 91, 66, 8, + 218, 156, 107, 6, 1, 25, 203, 84, 81, 244, 174, 193, + 70, 65, 104, 175, 63, 242, 110, 113, 154, 114, 80, 238, + 62, 170, 237, 126, 57, 99, 198, 187, 196, 71, 16, 48, + 5, 197, 136, 239, 8, 232, 231, 190, 123, 252, 35, 240, + 128, 180, 59, 75, 140, 131, 152, 22, 24, 138, 198, 253, + 120, 243, 81, 207, 179, 70, 127, 228, 160, 9, 11, 45, + 175, 25, 107, 12, 70, 110, 163, 159, 87, 229, 191, 174, + 53, 121, 189, 202, 238, 89, 251, 93, 69, 253, 146, 106, + 89, 195, 216, 103, 153, 69, 98, 165, 77, 64, 193, 84, + 65, 50, 115, 143, 207, 66, 131, 48, 32, 35, 231, 18, + 207, 52, 23, 16, 99, 65, 61, 135, 179, 146, 254, 94, + 101, 78, 250, 43, 2, 243, 222, 156, 133, 114, 222, 183, + 242, 43, 201, 139, 45, 17, 112, 141, 203, 29, 174, 184, + 2, 110, 46, 161, 63, 180, 0, 56, 70, 38, 47, 57, + 207, 246, 191, 248, 73, 37, 151, 106, 197, 219, 125, 206, + 11, 163, 95, 219, 252, 51, 214, 103, 174, 148, 157, 163, + 51, 107, 29, 53, 168, 54, 156, 187, 72, 57, 71, 163, + 75, 214, 21, 120, 201, 187, 45, 50, 223, 216, 215, 117, + 188, 136, 39, 146, 175, 9, 188, 9, 190, 20, 177, 137, + 59, 41, 215, 144, 47, 20, 51, 100, 218, 12, 97, 130, + 188, 85, 170, 103, 21, 242, 90, 111, 215, 244, 3, 140, + 218, 53, 140, 218, 59, 97, 180, 95, 195, 104, 127, 39, + 140, 58, 53, 140, 58, 59, 97, 116, 80, 195, 232, 96, + 39, 140, 14, 107, 24, 29, 238, 132, 209, 81, 13, 163, + 163, 157, 48, 234, 214, 48, 234, 238, 132, 209, 113, 13, + 163, 227, 159, 97, 84, 61, 168, 175, 17, 220, 25, 177, + 68, 167, 192, 151, 131, 23, 52, 134, 55, 162, 106, 138, + 11, 124, 50, 5, 126, 103, 175, 47, 100, 184, 216, 58, + 223, 31, 16, 147, 145, 189, 105, 57, 235, 18, 160, 44, + 27, 109, 19, 180, 135, 58, 116, 19, 175, 233, 184, 65, + 100, 101, 179, 103, 216, 118, 243, 195, 243, 220, 142, 229, + 45, 227, 60, 143, 124, 199, 233, 108, 53, 160, 187, 21, + 67, 187, 251, 21, 51, 86, 2, 150, 134, 175, 5, 61, + 23, 125, 211, 143, 92, 171, 23, 113, 57, 19, 120, 155, + 186, 67, 48, 164, 70, 146, 28, 113, 76, 187, 100, 77, + 251, 13, 229, 59, 85, 202, 127, 182, 40, 215, 216, 149, + 221, 94, 89, 203, 102, 15, 109, 28, 76, 171, 95, 131, + 42, 151, 62, 75, 181, 15, 137, 235, 150, 18, 219, 2, + 127, 75, 252, 138, 150, 215, 105, 146, 40, 251, 71, 105, + 104, 69, 31, 165, 177, 109, 72, 189, 165, 251, 93, 191, + 127, 160, 251, 183, 119, 40, 91, 126, 247, 245, 187, 15, + 153, 159, 213, 107, 32, 66, 124, 42, 169, 181, 68, 191, + 77, 171, 239, 176, 191, 184, 210, 167, 255, 0, 80, 75, + 7, 8, 90, 249, 201, 152, 9, 3, 0, 0, 41, 15, + 0, 0, 80, 75, 3, 4, 20, 0, 8, 8, 8, 0, + 7, 140, 222, 74, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 18, 0, 0, 0, 119, 111, 114, 100, + 47, 110, 117, 109, 98, 101, 114, 105, 110, 103, 46, 120, + 109, 108, 213, 88, 75, 110, 219, 48, 16, 221, 247, 20, + 6, 247, 14, 101, 217, 113, 12, 35, 114, 80, 160, 48, + 208, 46, 218, 2, 77, 15, 64, 83, 148, 77, 128, 31, + 129, 164, 236, 100, 219, 67, 180, 55, 232, 162, 23, 232, + 125, 122, 129, 94, 161, 35, 74, 254, 40, 73, 85, 33, + 49, 80, 106, 69, 105, 222, 252, 248, 40, 204, 12, 117, + 125, 115, 39, 197, 96, 203, 140, 229, 90, 37, 104, 116, + 17, 161, 1, 83, 84, 167, 92, 173, 19, 244, 249, 118, + 57, 156, 161, 129, 117, 68, 165, 68, 104, 197, 18, 116, + 207, 44, 186, 89, 188, 186, 222, 205, 85, 33, 87, 204, + 128, 222, 0, 92, 40, 59, 223, 37, 104, 227, 92, 62, + 199, 216, 210, 13, 147, 196, 94, 232, 156, 41, 192, 50, + 109, 36, 113, 240, 106, 214, 120, 167, 77, 154, 27, 77, + 153, 181, 96, 41, 5, 142, 163, 104, 138, 37, 225, 10, + 213, 110, 116, 130, 10, 163, 230, 181, 143, 161, 228, 212, + 104, 171, 51, 55, 164, 90, 206, 117, 150, 113, 202, 234, + 101, 111, 97, 186, 4, 174, 76, 222, 104, 90, 72, 166, + 92, 21, 214, 48, 65, 28, 236, 219, 110, 120, 110, 247, + 222, 182, 109, 241, 183, 82, 160, 5, 236, 157, 172, 172, + 51, 132, 186, 247, 133, 28, 52, 222, 222, 166, 64, 162, + 87, 17, 91, 1, 16, 135, 37, 65, 145, 151, 0, 141, + 198, 129, 108, 75, 68, 169, 132, 23, 21, 137, 75, 121, + 16, 174, 10, 33, 152, 171, 16, 48, 188, 101, 119, 7, + 232, 247, 151, 159, 7, 249, 59, 186, 151, 10, 150, 213, + 234, 249, 71, 83, 46, 14, 114, 169, 215, 189, 14, 132, + 64, 240, 156, 107, 155, 160, 171, 56, 42, 213, 241, 81, + 145, 171, 20, 192, 210, 79, 133, 194, 203, 134, 168, 181, + 63, 254, 241, 116, 175, 93, 123, 55, 245, 178, 212, 202, + 217, 114, 227, 150, 114, 158, 160, 79, 247, 114, 165, 133, + 55, 125, 173, 108, 67, 64, 109, 3, 230, 10, 194, 164, + 44, 35, 133, 168, 243, 62, 248, 42, 53, 63, 192, 185, + 213, 218, 85, 92, 31, 16, 251, 77, 63, 228, 116, 244, + 114, 78, 127, 125, 251, 126, 6, 78, 71, 209, 172, 141, + 84, 15, 63, 135, 213, 19, 50, 142, 204, 54, 133, 15, + 56, 59, 51, 195, 241, 25, 24, 254, 250, 227, 28, 12, + 79, 38, 173, 12, 151, 112, 47, 25, 30, 135, 82, 23, + 70, 179, 168, 149, 225, 18, 238, 77, 101, 152, 132, 82, + 25, 226, 209, 180, 141, 85, 15, 247, 242, 187, 189, 12, + 165, 50, 196, 151, 173, 13, 205, 195, 189, 100, 120, 26, + 74, 101, 136, 103, 173, 221, 205, 195, 189, 169, 12, 87, + 161, 84, 134, 113, 220, 218, 209, 60, 220, 203, 239, 118, + 22, 74, 101, 0, 198, 90, 25, 158, 62, 179, 163, 253, + 7, 134, 113, 227, 134, 241, 207, 235, 71, 252, 236, 235, + 135, 130, 27, 94, 37, 183, 69, 150, 29, 165, 14, 242, + 95, 63, 121, 86, 93, 14, 234, 148, 246, 38, 231, 13, + 198, 95, 54, 229, 7, 152, 122, 215, 241, 57, 192, 212, + 187, 206, 165, 1, 166, 222, 117, 248, 11, 48, 245, 174, + 83, 85, 128, 169, 119, 29, 87, 2, 76, 189, 235, 76, + 16, 96, 234, 93, 155, 109, 16, 169, 63, 238, 98, 202, + 119, 47, 117, 250, 211, 172, 209, 202, 26, 123, 194, 94, + 243, 145, 89, 252, 119, 179, 248, 212, 12, 159, 252, 175, + 92, 252, 1, 80, 75, 7, 8, 15, 16, 133, 144, 101, + 2, 0, 0, 245, 20, 0, 0, 80, 75, 3, 4, 20, + 0, 8, 8, 8, 0, 7, 140, 222, 74, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, 0, + 0, 119, 111, 114, 100, 47, 115, 101, 116, 116, 105, 110, + 103, 115, 46, 120, 109, 108, 69, 79, 203, 78, 195, 64, + 12, 188, 243, 21, 145, 239, 116, 23, 14, 60, 162, 36, + 21, 151, 158, 184, 81, 62, 192, 77, 220, 116, 165, 172, + 189, 90, 155, 6, 248, 122, 12, 85, 197, 109, 70, 243, + 208, 76, 183, 253, 204, 75, 115, 166, 170, 73, 184, 135, + 187, 77, 132, 134, 120, 148, 41, 241, 220, 195, 251, 126, + 119, 251, 4, 141, 26, 242, 132, 139, 48, 245, 240, 69, + 10, 219, 225, 166, 91, 91, 37, 51, 119, 105, 227, 13, + 172, 237, 218, 195, 201, 172, 180, 33, 232, 120, 162, 140, + 186, 145, 66, 236, 218, 81, 106, 70, 115, 90, 231, 176, + 74, 157, 74, 149, 145, 84, 61, 154, 151, 112, 31, 227, + 67, 200, 152, 24, 6, 175, 252, 22, 201, 205, 218, 22, + 170, 35, 177, 249, 156, 24, 33, 252, 10, 19, 29, 241, + 99, 177, 61, 30, 222, 76, 138, 91, 206, 184, 244, 240, + 24, 159, 47, 242, 40, 185, 160, 13, 93, 248, 135, 107, + 107, 190, 130, 118, 194, 246, 138, 60, 95, 35, 224, 128, + 80, 237, 69, 19, 94, 216, 33, 77, 201, 81, 248, 75, + 95, 63, 13, 63, 80, 75, 7, 8, 201, 163, 110, 75, + 202, 0, 0, 0, 24, 1, 0, 0, 80, 75, 3, 4, + 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 17, 0, + 0, 0, 119, 111, 114, 100, 47, 100, 111, 99, 117, 109, + 101, 110, 116, 46, 120, 109, 108, 237, 87, 219, 78, 227, + 48, 16, 125, 223, 175, 48, 121, 167, 105, 216, 46, 130, + 136, 22, 129, 184, 85, 98, 161, 82, 131, 208, 62, 186, + 142, 147, 88, 235, 155, 198, 78, 75, 249, 122, 236, 230, + 198, 101, 133, 186, 116, 197, 238, 34, 94, 234, 198, 51, + 115, 230, 204, 153, 177, 18, 31, 28, 222, 9, 142, 230, + 20, 12, 83, 114, 24, 68, 189, 126, 128, 168, 36, 42, + 101, 50, 31, 6, 55, 201, 217, 246, 94, 128, 140, 197, + 50, 197, 92, 73, 58, 12, 150, 212, 4, 135, 163, 47, + 7, 139, 56, 85, 164, 20, 84, 90, 228, 16, 164, 137, + 213, 48, 40, 65, 198, 134, 20, 84, 96, 179, 45, 24, + 1, 101, 84, 102, 183, 137, 18, 177, 202, 50, 70, 104, + 189, 4, 117, 4, 12, 131, 194, 90, 29, 135, 97, 29, + 212, 83, 154, 74, 103, 203, 20, 8, 108, 221, 35, 228, + 97, 21, 114, 82, 231, 10, 119, 250, 253, 221, 16, 40, + 199, 214, 241, 53, 5, 211, 166, 65, 155, 191, 150, 127, + 46, 120, 227, 183, 88, 39, 235, 66, 65, 170, 65, 17, + 106, 140, 19, 66, 240, 42, 175, 192, 76, 182, 48, 81, + 127, 141, 130, 61, 78, 27, 161, 215, 201, 156, 2, 94, + 60, 74, 249, 148, 200, 73, 101, 236, 16, 205, 11, 200, + 150, 70, 207, 209, 168, 213, 91, 161, 56, 188, 168, 255, + 12, 111, 90, 96, 77, 59, 180, 124, 51, 180, 115, 80, + 165, 110, 208, 4, 89, 167, 90, 129, 225, 103, 169, 189, + 98, 218, 117, 116, 198, 56, 179, 203, 85, 225, 29, 169, + 104, 176, 25, 171, 231, 154, 189, 13, 207, 207, 143, 32, + 241, 56, 151, 10, 240, 140, 187, 131, 224, 128, 144, 103, + 23, 140, 220, 89, 152, 169, 116, 233, 87, 189, 250, 153, + 192, 106, 153, 218, 37, 167, 104, 17, 207, 49, 31, 6, + 87, 190, 106, 30, 132, 222, 2, 222, 33, 236, 214, 58, + 0, 90, 155, 91, 206, 148, 180, 198, 5, 19, 215, 226, + 11, 202, 231, 212, 50, 130, 3, 183, 129, 13, 97, 236, + 217, 94, 113, 36, 205, 147, 189, 85, 26, 115, 223, 36, + 223, 25, 248, 157, 176, 69, 183, 35, 231, 202, 21, 186, + 85, 192, 211, 158, 55, 216, 202, 92, 209, 249, 205, 74, + 30, 177, 93, 139, 220, 139, 170, 94, 101, 251, 254, 250, + 188, 89, 136, 191, 219, 210, 164, 96, 230, 73, 43, 223, + 53, 189, 63, 94, 177, 209, 152, 184, 163, 161, 129, 26, + 10, 115, 26, 140, 16, 51, 200, 22, 20, 1, 197, 28, + 113, 150, 209, 207, 97, 251, 24, 195, 54, 182, 190, 181, + 82, 89, 148, 97, 105, 177, 89, 126, 54, 246, 99, 52, + 246, 72, 160, 49, 34, 128, 239, 151, 135, 159, 29, 221, + 188, 163, 178, 20, 149, 11, 227, 115, 222, 56, 244, 91, + 219, 56, 109, 246, 162, 58, 83, 27, 240, 15, 204, 194, + 241, 205, 229, 229, 105, 130, 174, 175, 78, 55, 24, 133, + 143, 160, 64, 114, 123, 253, 167, 21, 136, 254, 19, 5, + 46, 174, 47, 127, 160, 233, 197, 56, 217, 66, 211, 155, + 99, 84, 9, 178, 245, 75, 53, 12, 37, 182, 14, 91, + 234, 86, 9, 73, 239, 236, 4, 231, 180, 74, 165, 243, + 169, 79, 230, 46, 97, 81, 180, 239, 63, 243, 29, 39, + 247, 127, 119, 239, 235, 94, 227, 240, 29, 131, 219, 229, + 52, 179, 206, 48, 24, 244, 189, 15, 176, 188, 120, 244, + 88, 80, 156, 82, 240, 99, 228, 30, 172, 210, 157, 37, + 83, 202, 182, 150, 153, 178, 86, 137, 206, 152, 151, 182, + 54, 214, 169, 174, 74, 145, 84, 84, 51, 225, 224, 83, + 74, 88, 219, 53, 127, 77, 153, 128, 123, 193, 213, 117, + 100, 152, 155, 186, 8, 235, 74, 58, 97, 224, 202, 117, + 151, 208, 198, 206, 33, 153, 85, 102, 119, 49, 62, 7, + 230, 187, 234, 117, 240, 176, 25, 46, 185, 245, 12, 56, + 147, 116, 194, 44, 41, 188, 212, 43, 78, 164, 192, 48, + 173, 62, 155, 6, 59, 251, 131, 253, 221, 40, 250, 214, + 204, 65, 35, 104, 216, 92, 48, 194, 238, 214, 61, 122, + 0, 80, 75, 7, 8, 175, 55, 157, 93, 214, 2, 0, + 0, 186, 15, 0, 0, 80, 75, 3, 4, 20, 0, 8, + 8, 8, 0, 7, 140, 222, 74, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 119, + 111, 114, 100, 47, 116, 104, 101, 109, 101, 47, 116, 104, + 101, 109, 101, 49, 46, 120, 109, 108, 229, 88, 75, 111, + 227, 54, 16, 190, 247, 87, 16, 186, 239, 210, 122, 57, + 114, 16, 103, 177, 118, 44, 244, 208, 22, 69, 226, 162, + 103, 90, 162, 37, 109, 168, 7, 72, 38, 78, 254, 125, + 135, 212, 139, 178, 172, 196, 187, 241, 162, 5, 234, 131, + 77, 82, 223, 204, 55, 15, 114, 56, 242, 205, 151, 151, + 156, 161, 103, 202, 69, 86, 22, 75, 203, 254, 60, 179, + 16, 45, 162, 50, 206, 138, 100, 105, 253, 181, 13, 63, + 5, 214, 151, 219, 95, 110, 200, 181, 76, 105, 78, 17, + 160, 11, 113, 77, 150, 86, 42, 101, 117, 141, 177, 136, + 96, 153, 136, 207, 101, 69, 11, 120, 182, 47, 121, 78, + 36, 76, 121, 130, 99, 78, 14, 160, 37, 103, 216, 153, + 205, 230, 56, 39, 89, 97, 161, 130, 228, 116, 105, 221, + 209, 61, 121, 98, 18, 109, 149, 78, 235, 182, 213, 190, + 97, 240, 85, 72, 161, 22, 34, 198, 31, 34, 77, 57, + 16, 209, 224, 248, 209, 86, 63, 226, 85, 172, 25, 71, + 207, 132, 45, 45, 96, 138, 203, 195, 150, 190, 72, 11, + 49, 34, 36, 60, 88, 90, 51, 253, 177, 240, 237, 13, + 238, 132, 152, 156, 144, 53, 228, 66, 253, 105, 228, 26, + 129, 248, 209, 209, 114, 60, 217, 117, 130, 118, 232, 45, + 174, 238, 58, 253, 78, 173, 127, 140, 219, 108, 54, 235, + 141, 221, 233, 211, 0, 18, 69, 224, 170, 61, 194, 122, + 97, 96, 175, 90, 157, 6, 168, 30, 142, 117, 175, 103, + 254, 204, 27, 226, 13, 253, 238, 8, 191, 88, 173, 86, + 254, 98, 128, 119, 123, 188, 55, 194, 7, 179, 185, 247, + 213, 25, 224, 189, 30, 239, 143, 237, 95, 125, 93, 175, + 231, 3, 188, 223, 227, 231, 35, 124, 120, 181, 152, 123, + 67, 188, 6, 165, 44, 43, 30, 71, 104, 149, 207, 46, + 51, 29, 100, 95, 178, 95, 79, 194, 3, 128, 7, 237, + 6, 232, 81, 216, 216, 94, 181, 124, 33, 39, 55, 91, + 78, 190, 149, 60, 4, 132, 206, 46, 145, 89, 129, 228, + 107, 5, 128, 8, 128, 219, 44, 167, 2, 253, 65, 15, + 232, 190, 204, 73, 161, 152, 200, 53, 37, 6, 162, 94, + 138, 196, 209, 18, 62, 82, 156, 103, 197, 79, 98, 233, + 21, 99, 211, 83, 237, 119, 62, 237, 246, 62, 99, 236, + 65, 190, 50, 250, 155, 208, 54, 137, 146, 101, 113, 8, + 139, 122, 162, 165, 186, 48, 87, 41, 12, 27, 190, 1, + 46, 225, 68, 143, 17, 47, 229, 223, 153, 76, 31, 82, + 82, 1, 143, 173, 25, 18, 209, 168, 78, 4, 170, 74, + 1, 201, 181, 38, 117, 235, 18, 145, 21, 178, 94, 243, + 219, 99, 13, 104, 34, 127, 47, 227, 122, 217, 53, 143, + 123, 167, 70, 207, 18, 97, 18, 185, 74, 193, 185, 100, + 238, 213, 199, 200, 236, 26, 120, 38, 155, 237, 159, 102, + 243, 223, 100, 195, 70, 52, 97, 139, 35, 162, 138, 183, + 61, 119, 106, 106, 36, 34, 194, 104, 172, 226, 94, 43, + 104, 211, 114, 241, 20, 137, 148, 196, 180, 201, 145, 125, + 210, 17, 219, 61, 51, 108, 193, 251, 81, 51, 216, 22, + 238, 199, 216, 206, 73, 146, 73, 231, 77, 208, 249, 23, + 200, 210, 108, 148, 37, 60, 62, 142, 172, 24, 206, 208, + 1, 172, 242, 29, 223, 66, 17, 169, 150, 214, 30, 74, + 8, 12, 243, 10, 244, 137, 34, 177, 16, 97, 9, 92, + 239, 145, 108, 92, 121, 247, 48, 31, 59, 124, 122, 91, + 218, 179, 73, 135, 7, 20, 21, 23, 242, 142, 136, 180, + 150, 210, 143, 218, 219, 176, 232, 237, 119, 124, 79, 197, + 225, 50, 14, 156, 168, 70, 231, 89, 225, 6, 246, 191, + 104, 5, 62, 78, 45, 221, 239, 105, 36, 39, 86, 250, + 105, 243, 172, 124, 146, 148, 63, 164, 241, 1, 237, 216, + 19, 191, 39, 96, 183, 87, 239, 174, 56, 19, 18, 66, + 220, 78, 160, 203, 241, 189, 102, 227, 13, 79, 126, 115, + 10, 142, 111, 221, 230, 116, 16, 86, 165, 164, 169, 73, + 129, 145, 251, 26, 174, 199, 157, 13, 122, 102, 152, 135, + 39, 108, 255, 65, 87, 220, 11, 186, 226, 255, 127, 93, + 81, 59, 151, 22, 212, 141, 117, 7, 1, 125, 0, 39, + 72, 237, 209, 165, 85, 114, 153, 150, 80, 133, 170, 52, + 139, 66, 14, 157, 131, 230, 2, 187, 160, 83, 150, 202, + 36, 196, 212, 59, 131, 178, 149, 62, 247, 117, 171, 214, + 81, 23, 185, 36, 149, 247, 89, 130, 120, 6, 149, 78, + 166, 156, 210, 63, 101, 227, 231, 59, 202, 108, 199, 188, + 95, 91, 69, 77, 157, 233, 204, 21, 85, 253, 187, 163, + 207, 148, 109, 213, 233, 157, 43, 255, 45, 148, 182, 213, + 164, 9, 132, 198, 29, 39, 13, 159, 58, 93, 187, 36, + 252, 15, 119, 62, 222, 68, 231, 243, 118, 123, 208, 19, + 121, 223, 211, 139, 120, 70, 209, 55, 174, 130, 197, 199, + 76, 248, 206, 171, 214, 57, 237, 177, 227, 159, 125, 213, + 86, 68, 166, 72, 125, 65, 225, 206, 120, 196, 104, 215, + 223, 110, 203, 123, 200, 62, 234, 58, 74, 4, 27, 241, + 83, 208, 28, 191, 110, 113, 7, 54, 7, 134, 115, 74, + 213, 207, 109, 163, 250, 20, 4, 19, 249, 190, 100, 243, + 105, 4, 219, 157, 8, 246, 219, 116, 63, 30, 108, 255, + 68, 172, 253, 183, 67, 141, 199, 71, 20, 27, 111, 50, + 122, 54, 250, 51, 161, 220, 125, 3, 238, 230, 245, 70, + 212, 175, 79, 47, 146, 147, 117, 251, 22, 8, 122, 112, + 47, 122, 251, 15, 80, 75, 7, 8, 58, 137, 128, 168, + 212, 3, 0, 0, 22, 17, 0, 0, 80, 75, 3, 4, + 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 0, + 0, 0, 119, 111, 114, 100, 47, 102, 111, 110, 116, 84, + 97, 98, 108, 101, 46, 120, 109, 108, 197, 147, 221, 106, + 194, 64, 16, 133, 239, 251, 20, 203, 222, 215, 141, 94, + 148, 18, 140, 82, 40, 165, 23, 173, 133, 170, 15, 48, + 110, 38, 102, 97, 127, 194, 206, 38, 169, 111, 223, 53, + 42, 136, 141, 208, 22, 209, 187, 236, 206, 236, 57, 223, + 28, 38, 227, 233, 151, 209, 172, 65, 79, 202, 217, 140, + 15, 7, 9, 103, 104, 165, 203, 149, 93, 103, 124, 185, + 120, 185, 127, 228, 140, 2, 216, 28, 180, 179, 152, 241, + 13, 18, 159, 78, 238, 198, 109, 90, 56, 27, 136, 197, + 231, 150, 210, 54, 227, 101, 8, 85, 42, 4, 201, 18, + 13, 208, 192, 85, 104, 99, 173, 112, 222, 64, 136, 71, + 191, 22, 173, 243, 121, 229, 157, 68, 162, 168, 110, 180, + 24, 37, 201, 131, 48, 160, 44, 223, 203, 248, 223, 200, + 184, 162, 80, 18, 159, 157, 172, 13, 218, 176, 19, 241, + 168, 33, 196, 9, 168, 84, 21, 241, 201, 158, 142, 181, + 169, 5, 19, 161, 23, 202, 32, 177, 25, 182, 236, 211, + 25, 176, 93, 131, 44, 193, 19, 110, 123, 26, 208, 25, + 79, 18, 46, 186, 119, 96, 148, 222, 28, 110, 125, 215, + 222, 21, 42, 21, 100, 121, 184, 111, 192, 43, 88, 105, + 220, 150, 196, 206, 236, 135, 233, 124, 99, 86, 78, 247, + 122, 141, 46, 237, 245, 20, 91, 250, 173, 122, 199, 162, + 86, 17, 253, 211, 234, 77, 173, 208, 119, 97, 179, 57, + 122, 85, 116, 174, 160, 195, 44, 86, 15, 58, 167, 121, + 139, 62, 178, 225, 165, 67, 248, 136, 187, 114, 20, 250, + 9, 83, 23, 17, 91, 90, 21, 151, 27, 217, 251, 252, + 74, 80, 199, 113, 129, 165, 179, 100, 87, 194, 121, 69, + 221, 96, 80, 18, 110, 13, 242, 215, 191, 3, 234, 224, + 122, 172, 114, 44, 160, 214, 225, 38, 107, 113, 6, 169, + 119, 250, 253, 7, 77, 190, 1, 80, 75, 7, 8, 27, + 51, 25, 108, 79, 1, 0, 0, 113, 5, 0, 0, 80, + 75, 3, 4, 20, 0, 8, 8, 8, 0, 7, 140, 222, + 74, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 19, 0, 0, 0, 91, 67, 111, 110, 116, 101, 110, + 116, 95, 84, 121, 112, 101, 115, 93, 46, 120, 109, 108, + 189, 148, 205, 78, 195, 48, 16, 132, 239, 125, 138, 200, + 87, 148, 184, 229, 128, 16, 74, 218, 3, 18, 71, 232, + 161, 156, 145, 107, 111, 90, 139, 248, 71, 94, 183, 180, + 111, 207, 58, 13, 17, 66, 72, 105, 105, 203, 197, 82, + 228, 153, 249, 198, 155, 196, 229, 108, 103, 154, 108, 11, + 1, 181, 179, 21, 155, 20, 99, 150, 129, 149, 78, 105, + 187, 170, 216, 235, 226, 41, 191, 103, 179, 233, 168, 92, + 236, 61, 96, 70, 90, 139, 21, 91, 199, 232, 31, 56, + 71, 185, 6, 35, 176, 112, 30, 44, 237, 212, 46, 24, + 17, 233, 49, 172, 184, 23, 242, 93, 172, 128, 223, 142, + 199, 119, 92, 58, 27, 193, 198, 60, 166, 12, 54, 45, + 95, 8, 23, 180, 130, 108, 46, 66, 124, 22, 6, 42, + 198, 223, 2, 52, 200, 139, 180, 178, 236, 241, 96, 72, + 204, 138, 9, 239, 27, 45, 69, 164, 126, 124, 107, 213, + 15, 90, 222, 145, 146, 179, 213, 224, 90, 123, 188, 33, + 1, 227, 191, 147, 148, 147, 243, 224, 60, 114, 10, 46, + 146, 238, 36, 156, 171, 107, 45, 129, 50, 54, 134, 44, + 5, 236, 200, 169, 64, 229, 158, 34, 33, 68, 13, 199, + 177, 165, 11, 112, 58, 252, 235, 172, 201, 125, 36, 241, + 195, 5, 213, 13, 183, 47, 77, 242, 255, 24, 116, 139, + 198, 184, 111, 0, 207, 158, 115, 202, 162, 3, 75, 64, + 164, 15, 147, 250, 31, 114, 7, 241, 118, 99, 150, 16, + 200, 114, 249, 6, 125, 244, 240, 12, 32, 70, 210, 93, + 99, 10, 93, 242, 96, 133, 239, 239, 254, 178, 21, 250, + 29, 35, 180, 29, 236, 17, 233, 198, 128, 195, 58, 57, + 187, 75, 27, 51, 136, 172, 9, 176, 16, 203, 230, 15, + 255, 219, 208, 217, 251, 232, 174, 196, 168, 228, 237, 61, + 57, 253, 4, 80, 75, 7, 8, 113, 70, 144, 68, 71, + 1, 0, 0, 86, 5, 0, 0, 80, 75, 1, 2, 20, + 0, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 232, + 208, 1, 35, 217, 0, 0, 0, 61, 2, 0, 0, 11, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 95, 114, 101, 108, 115, 47, 46, + 114, 101, 108, 115, 80, 75, 1, 2, 20, 0, 20, 0, + 8, 8, 8, 0, 7, 140, 222, 74, 126, 47, 253, 199, + 37, 1, 0, 0, 0, 2, 0, 0, 16, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 18, 1, + 0, 0, 100, 111, 99, 80, 114, 111, 112, 115, 47, 97, + 112, 112, 46, 120, 109, 108, 80, 75, 1, 2, 20, 0, + 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 45, 164, + 180, 116, 82, 1, 0, 0, 138, 2, 0, 0, 17, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 117, 2, 0, 0, 100, 111, 99, 80, 114, 111, 112, 115, + 47, 99, 111, 114, 101, 46, 120, 109, 108, 80, 75, 1, + 2, 20, 0, 20, 0, 8, 8, 8, 0, 7, 140, 222, + 74, 41, 11, 4, 96, 232, 0, 0, 0, 28, 3, 0, + 0, 28, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 6, 4, 0, 0, 119, 111, 114, 100, 47, + 95, 114, 101, 108, 115, 47, 100, 111, 99, 117, 109, 101, + 110, 116, 46, 120, 109, 108, 46, 114, 101, 108, 115, 80, + 75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 7, + 140, 222, 74, 90, 249, 201, 152, 9, 3, 0, 0, 41, + 15, 0, 0, 15, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 56, 5, 0, 0, 119, 111, 114, + 100, 47, 115, 116, 121, 108, 101, 115, 46, 120, 109, 108, + 80, 75, 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, + 7, 140, 222, 74, 15, 16, 133, 144, 101, 2, 0, 0, + 245, 20, 0, 0, 18, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 126, 8, 0, 0, 119, 111, + 114, 100, 47, 110, 117, 109, 98, 101, 114, 105, 110, 103, + 46, 120, 109, 108, 80, 75, 1, 2, 20, 0, 20, 0, + 8, 8, 8, 0, 7, 140, 222, 74, 201, 163, 110, 75, + 202, 0, 0, 0, 24, 1, 0, 0, 17, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 35, 11, + 0, 0, 119, 111, 114, 100, 47, 115, 101, 116, 116, 105, + 110, 103, 115, 46, 120, 109, 108, 80, 75, 1, 2, 20, + 0, 20, 0, 8, 8, 8, 0, 7, 140, 222, 74, 175, + 55, 157, 93, 214, 2, 0, 0, 186, 15, 0, 0, 17, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 44, 12, 0, 0, 119, 111, 114, 100, 47, 100, 111, + 99, 117, 109, 101, 110, 116, 46, 120, 109, 108, 80, 75, + 1, 2, 20, 0, 20, 0, 8, 8, 8, 0, 7, 140, + 222, 74, 58, 137, 128, 168, 212, 3, 0, 0, 22, 17, + 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 65, 15, 0, 0, 119, 111, 114, 100, + 47, 116, 104, 101, 109, 101, 47, 116, 104, 101, 109, 101, + 49, 46, 120, 109, 108, 80, 75, 1, 2, 20, 0, 20, + 0, 8, 8, 8, 0, 7, 140, 222, 74, 27, 51, 25, + 108, 79, 1, 0, 0, 113, 5, 0, 0, 18, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 88, + 19, 0, 0, 119, 111, 114, 100, 47, 102, 111, 110, 116, + 84, 97, 98, 108, 101, 46, 120, 109, 108, 80, 75, 1, + 2, 20, 0, 20, 0, 8, 8, 8, 0, 7, 140, 222, + 74, 113, 70, 144, 68, 71, 1, 0, 0, 86, 5, 0, + 0, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 231, 20, 0, 0, 91, 67, 111, 110, 116, + 101, 110, 116, 95, 84, 121, 112, 101, 115, 93, 46, 120, + 109, 108, 80, 75, 5, 6, 0, 0, 0, 0, 11, 0, + 11, 0, 191, 2, 0, 0, 111, 22, 0, 0, 0, 0} + +func TestNewDocx(t *testing.T) { + _, err := NewDocx(bytes.NewReader(testDocx), int64(len(testDocx))) + if err != nil { + t.Fatal("Failed to open valid docx data", err) + } + + _, err = NewDocx(bytes.NewReader([]byte{255}), 1) + if err == nil { + t.Error("Failed to get error with bad zip data") + } + + buf := new(bytes.Buffer) + zipWriter := zip.NewWriter(buf) + fileWriter, err := zipWriter.Create("randomData") + if err != nil { + t.Fatal("Failed to create zip writer", err) + } + + io.CopyN(fileWriter, rand.Reader, 128) + zipWriter.Flush() + zipWriter.Close() + + _, err = NewDocx(bytes.NewReader(buf.Bytes()), int64(buf.Len())) + if err != ErrMissingDocument { + t.Error("Failed to get err", ErrMissingDocument, "got", err) + } +} + +func TestText(t *testing.T) { + doc, err := NewDocx(bytes.NewReader(testDocx), int64(len(testDocx))) + if err != nil { + t.Fatal("Failed to open valid docx data", err) + } + + lines, err := doc.Text() + if err != nil { + t.Error("Failed to get text lines from valid doc", err) + } + + expected := []string{ + "Hello World.", + "This", + "is the real life.", + "It is not fantasy.", + "Am I crazy?", + "BULLET ONE", + "BULLET TWO", + "HOLY SHIT! SUB BULLET!", + } + + if len(lines) != len(expected) { + t.Errorf("len(lines) != len(expected): %d != %d", len(lines), len(expected)) + } + + for i, line := range lines { + if line != expected[i] { + t.Errorf("line != expected[%d]: \"%s\" != \"%s\"", i, line, expected[i]) + } + } +} diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..3269730 --- /dev/null +++ b/handler.go @@ -0,0 +1,534 @@ +package main + +import ( + "bufio" + "bytes" + "crypto/sha256" + "encoding/hex" + "fmt" + "html" + "html/template" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "path/filepath" + "strings" + + "github.com/blevesearch/bleve" + blevemapping "github.com/blevesearch/bleve/mapping" + log "github.com/sirupsen/logrus" + "github.com/tblyler/recipe-card/recipe" +) + +const ( + imagePattern = "/images/" + stockImagePatten = "/stock-images/" + recipePattern = "/recipe/" + docxPattern = "/docx/" +) + +// Handler contains functions for http handlerfunc +type Handler struct { + recipePath string + recipes map[string]*recipe.Recipe + recipeSlice []*recipe.Recipe + idx bleve.Index + templates *template.Template + logger *log.Logger +} + +func GetItemIndex(path string) (map[string][]byte, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + + defer file.Close() + + reader := bufio.NewReader(file) + + itemIndex := make(map[string][]byte) + for { + data, err := reader.ReadBytes('\n') + if err != nil { + if err == io.EOF { + return itemIndex, nil + } + + return nil, err + } + + // remove newline + key := string(data[:len(data)-1]) + sha256sum := make([]byte, sha256.Size) + + read, err := reader.Read(sha256sum) + if err != nil && read != sha256.Size { + return nil, err + } + + itemIndex[key] = sha256sum + if err == io.EOF { + break + } + } + + return itemIndex, nil +} + +func SaveItemIndex(itemIndex map[string][]byte, path string) error { + file, err := os.Create(path) + if err != nil { + return err + } + + defer file.Close() + + for key, sha256sum := range itemIndex { + _, err = file.WriteString(key + "\n") + if err != nil { + return err + } + + _, err = file.Write(sha256sum) + if err != nil { + return err + } + } + + return nil +} + +// NewHandler creates a new instance to handle HTTP requests +func NewHandler(recipePath string, indexPath string, logger *log.Logger) (*Handler, error) { + if logger == nil { + logger = log.New() + logger.Out = ioutil.Discard + } + + logger.WithField("recipePath", recipePath).Debugln("Getting absolute recipe path") + + var err error + recipePath, err = filepath.Abs(recipePath) + if err != nil { + return nil, fmt.Errorf("Failed absolute recipe path: %s", err.Error()) + } + + logger.WithField("recipePath", recipePath).Debugln("Got absolute recipe path") + + if indexPath != "" { + logger.WithField("indexPath", indexPath).Debugln("Getting absolute index path") + indexPath, err = filepath.Abs(indexPath) + if err != nil { + return nil, fmt.Errorf("Failed absolute index path: %s", err.Error()) + } + + logger.WithField("indexPath", indexPath).Debugln("Got absolute index path") + os.MkdirAll(indexPath, 0755) + } + + logger.WithField("recipePath", recipePath).Infoln("Getting recipes from path") + recipeSlice, err := recipe.RecipesFromPath(recipePath) + if err != nil { + return nil, err + } + + logger.Infof("Found %d recipes", len(recipeSlice)) + + handler := new(Handler) + handler.logger = logger + handler.recipePath = recipePath + handler.recipeSlice = recipeSlice + handler.recipes = make(map[string]*recipe.Recipe) + bleveIndexPath := "" + itemIndexPath := "" + // this improves indexing performance a shit ton + // I don't think it stores the document data, just analysis data + // could be wrong, documentation is sparse for it + // functionality seems the same for here though + blevemapping.StoreDynamic = false + + if indexPath == "" { + logger.Info("Creating memory mapped search index") + handler.idx, err = bleve.NewMemOnly(bleve.NewIndexMapping()) + } else { + itemIndexPath = filepath.Join(indexPath, "item.idx") + bleveIndexPath = filepath.Join(indexPath, "bleve") + logger.WithField("bleveIndexPath", bleveIndexPath).Infoln("Trying to open index path") + + handler.idx, err = bleve.Open(bleveIndexPath) + if err != nil { + logger.WithError(err).WithField("bleveIndexPath", bleveIndexPath).Warnln( + "Failed to open index path, trying to recreate it", + ) + + handler.idx, err = bleve.New(bleveIndexPath, bleve.NewIndexMapping()) + } + } + + if err != nil { + return nil, fmt.Errorf("Bleve open: %s", err.Error()) + } + + var itemIndex map[string][]byte + + if itemIndexPath != "" { + logger.WithField("itemIndexPath", itemIndexPath).Debugln("Trying to open previous item index") + itemIndex, err = GetItemIndex(itemIndexPath) + if err != nil { + logger.WithError(err).WithField("itemIndexPath", itemIndexPath).Warnln("Failed to open previous item index") + } else { + logger.WithField("count", len(itemIndex)).Debugln("Got previous item indexes") + } + } + + if itemIndex == nil { + itemIndex = make(map[string][]byte) + } + + for _, recip := range recipeSlice { + if recip.Title == "" { + logger.WithField("docx", recip.DocxPath).Errorln( + "Missing title", + ) + continue + } + + if oldRecip, exists := handler.recipes[recip.Title]; exists { + logger.WithFields(log.Fields{ + "existingPath": oldRecip.DocxPath, + "newPath": recip.DocxPath, + "title": recip.Title, + }).Errorln("Duplicate recipe title") + continue + } + + handler.recipes[recip.Title] = recip + + logger.WithField("recipeTitle", recip.Title).Debugln("Hashing data") + hasher := sha256.New() + io.WriteString(hasher, recip.Title) + for _, order := range recipe.ValidCategoriesOrder { + if info, exists := recip.Info[order]; exists { + for _, line := range info { + io.WriteString(hasher, line) + } + } + } + + sha256sum := hasher.Sum(nil) + + logger.WithFields(log.Fields{ + "recipeTitle": recip.Title, + "sha256": hex.EncodeToString(sha256sum), + }).Debugln("Finished hashing data") + + if oldSha, exists := itemIndex[recip.Title]; !exists || !bytes.Equal(sha256sum, oldSha) { + logger.WithFields(log.Fields{ + "recipeTitle": recip.Title, + "docx": recip.DocxPath, + }).Infoln("Indexing") + + if exists { + handler.idx.Delete(recip.Title) + } + + itemIndex[recip.Title] = sha256sum + err = handler.idx.Index(recip.Title, recip) + if err != nil { + return nil, fmt.Errorf("Index fail: %s", err.Error()) + } + + logger.WithField("recipeTitle", recip.Title).Infoln("Indexed") + } + } + + for recipeTitle := range itemIndex { + if _, exists := handler.recipes[recipeTitle]; !exists { + logger.WithField("recipeTitle", recipeTitle).Infoln("Removing missing recipe") + handler.idx.Delete(recipeTitle) + delete(itemIndex, recipeTitle) + } + } + + err = SaveItemIndex(itemIndex, itemIndexPath) + if err != nil { + logger.WithError(err).WithField("itemIndexPath", itemIndexPath).Warnln( + "Failed to update index data", + ) + } else { + logger.Infoln("Updated index data") + } + + handler.templates, err = NewTemplate(logger) + if err != nil { + return nil, err + } + + return handler, nil +} + +// Close handler and free up memory +func (h *Handler) Close() error { + h.recipes = nil + return h.idx.Close() +} + +// GetHandlerFuncs in a pattern->func map +func (h *Handler) GetHandlerFuncs() map[string]http.HandlerFunc { + return map[string]http.HandlerFunc{ + "/": h.Index, + "/search/": h.Search, + "/recipes/": h.Recipes, + recipePattern: h.Recipe, + "/css/mini.css": h.MiniCSS, + "/css/main.css": h.MainCSS, + imagePattern: h.Images, + stockImagePatten: h.StockImages, + docxPattern: h.Docx, + } +} + +// MainCSS outputs the main css file information +func (h *Handler) MainCSS(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css") + + io.WriteString(w, maincss) +} + +// MiniCSS writes the mini css data to writer +func (h *Handler) MiniCSS(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/css") + + io.WriteString(w, minicss) +} + +// Index handles index request +func (h *Handler) Index(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + + tmplData := &TemplateData{ + PageTitle: "Recipe Card", + } + + h.templates.ExecuteTemplate(w, "index", tmplData) +} + +// Search handles search request +func (h *Handler) Search(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + search := strings.TrimSpace(r.PostFormValue("search")) + if search == "" { + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) + return + } + + tmplData := &TemplateData{ + PageTitle: "Recipe Card - Search", + SearchValue: search, + } + + searchResults, _ := h.idx.Search(bleve.NewSearchRequest(bleve.NewMatchQuery( + search, + ))) + + // try a fuzzy search if matchquery fails + if searchResults.Hits.Len() == 0 { + searchResults, _ = h.idx.Search(bleve.NewSearchRequest(bleve.NewFuzzyQuery( + search, + ))) + } + + for _, hit := range searchResults.Hits { + recipe := h.recipes[hit.ID] + + tmplData.Recipes = append( + tmplData.Recipes, + h.recipeToTemplateRecipe(recipe), + ) + } + + h.templates.ExecuteTemplate(w, "search", tmplData) +} + +// Recipes handles recipes page for all recipes +func (h *Handler) Recipes(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + + tmplData := &TemplateData{ + PageTitle: "Recipe Card - Recipes", + } + + for _, recipe := range h.recipeSlice { + tmplData.Recipes = append( + tmplData.Recipes, + h.recipeToTemplateRecipe(recipe), + ) + } + + h.templates.ExecuteTemplate(w, "recipes", tmplData) +} + +// Recipe handles a single recipe page +func (h *Handler) Recipe(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + + id := strings.TrimPrefix(r.URL.Path, recipePattern) + + if recipe, exists := h.recipes[id]; exists { + tmplData := &TemplateData{ + PageTitle: "Recipe Card - " + id, + } + + tmplData.Recipes = append( + tmplData.Recipes, + h.recipeToTemplateRecipe(recipe), + ) + + h.templates.ExecuteTemplate(w, "recipe", tmplData) + return + } + + http.Redirect(w, r, "/", http.StatusTemporaryRedirect) +} + +// StockImages handles all stock image requests +func (h *Handler) StockImages(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "image/jpeg") + + id := strings.TrimPrefix(strings.TrimSuffix(r.URL.Path, ".jpg"), stockImagePatten) + if recipe, exists := h.recipes[id]; exists { + w.Write(recipe.Image) + return + } + + w.WriteHeader(http.StatusNotFound) + return +} + +// Docx handles all docx download requests +func (h *Handler) Docx(w http.ResponseWriter, r *http.Request) { + lowerPath := strings.ToLower(r.URL.Path) + + if !strings.HasSuffix(lowerPath, "docx") { + w.WriteHeader(http.StatusNotFound) + return + } + + file, err := os.Open(h.urlToPath(r.URL.Path, docxPattern)) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + defer file.Close() + + w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.wordprocessingml.document") + io.Copy(w, file) +} + +// Images handles all image requests +func (h *Handler) Images(w http.ResponseWriter, r *http.Request) { + lowerPath := strings.ToLower(r.URL.Path) + + if !strings.HasSuffix(lowerPath, "jpg") && !strings.HasSuffix(lowerPath, "jpeg") { + w.WriteHeader(http.StatusNotFound) + return + } + + file, err := os.Open(h.urlToPath(r.URL.Path, imagePattern)) + if err != nil { + w.WriteHeader(http.StatusNotFound) + return + } + + defer file.Close() + + w.Header().Set("Content-Type", "image/jpeg") + io.Copy(w, file) +} + +func (h *Handler) pathToURL(filePath, pattern string) (string, error) { + path, err := filepath.Rel(h.recipePath, filePath) + if err != nil { + return "", err + } + + urlPath := pattern[:len(pattern)-1] + for _, pathPart := range strings.Split(path, string(filepath.Separator)) { + if pathPart == "" { + continue + } + + urlPath += "/" + url.PathEscape(pathPart) + } + + return urlPath, nil +} + +func (h *Handler) urlToPath(url, pattern string) string { + path := filepath.Join( + h.recipePath, + strings.Replace(strings.TrimPrefix(url, pattern), "/", string(filepath.Separator), -1), + ) + + if !filepath.IsAbs(path) { + log.WithFields(log.Fields{ + "url": url, + "pattern": pattern, + "path": path, + }).Errorln("Must only receive absolute path") + return "" + } + + return path +} + +// recipeToTemplateRecipe converts a recipe.Recipe to a TemplateRecipe +func (h *Handler) recipeToTemplateRecipe(rec *recipe.Recipe) *TemplateRecipe { + tmplRecipe := &TemplateRecipe{ + ID: rec.Title, + URL: "/recipe/" + url.PathEscape(rec.Title), + StockImage: stockImagePatten + url.PathEscape(rec.Title+".jpg"), + } + + docxURL, err := h.pathToURL(rec.DocxPath, docxPattern) + if err != nil { + log.WithError(err).WithField("docxPath", rec.DocxPath).Warnln( + "Failed to get docx url", + ) + } else { + tmplRecipe.DocxURL = docxURL + } + + for _, imagePath := range rec.ScanPaths { + urlPath, err := h.pathToURL(imagePath, imagePattern) + if err != nil { + continue + } + + tmplRecipe.Images = append( + tmplRecipe.Images, + urlPath, + ) + } + + for _, category := range recipe.ValidCategoriesOrder { + if info, exists := rec.Info[category]; exists { + description := "" + for _, infoLine := range info { + description += "

" + html.EscapeString(infoLine) + "

" + } + + tmplRecipe.Description += template.HTML(fmt.Sprintf( + "

%s

%s", + html.EscapeString(category), + description, + )) + } + } + + return tmplRecipe +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..44340e1 --- /dev/null +++ b/main.go @@ -0,0 +1,144 @@ +package main + +import ( + "fmt" + "net" + "net/http" + "os" + "path" + "path/filepath" + "time" + + log "github.com/sirupsen/logrus" + open "github.com/skratchdot/open-golang/open" + flag "github.com/spf13/pflag" +) + +// Mux is the http servemux for this server +type Mux struct { + mux *http.ServeMux +} + +// NewMux creates a new mux instance +func NewMux() *Mux { + return &Mux{ + mux: http.NewServeMux(), + } +} + +// Handle registers the handler for the given pattern +func (mux *Mux) Handle(pattern string, handler http.Handler) { + mux.mux.Handle(pattern, handler) +} + +// HandleFunc registers the handler func for the given pattern +func (mux *Mux) HandleFunc(pattern string, handler func(http.ResponseWriter, *http.Request)) { + mux.mux.HandleFunc(pattern, handler) +} + +// ServeHTTP dispatches the request to the handler whose pattern most closely matches the request URL +func (mux *Mux) ServeHTTP(w http.ResponseWriter, r *http.Request) { + log.WithFields(log.Fields{ + "remoteAddr": r.RemoteAddr, + "userAgent": r.UserAgent(), + "requestURI": r.RequestURI, + "method": r.Method, + }).Debug() + mux.mux.ServeHTTP(w, r) +} + +func main() { + // only output >= info by default + log.SetLevel(log.InfoLevel) + + // get good timestamps for logs + log.SetFormatter(&log.TextFormatter{ + FullTimestamp: true, + }) + + // get the path relative to the executble + recipePath, err := os.Executable() + if err != nil { + log.WithError(err).Errorln("Failed to get executable path") + recipePath = "" + } + + debug := false + listenAddr := "127.0.0.1" + listenPort := uint16(0) + indexPath := filepath.Join(path.Dir(recipePath), "search_idx") + recipePath = filepath.Join(path.Dir(recipePath), "Recipes") + recipePath, err = filepath.Abs(recipePath) + if err != nil { + log.WithError(err).Errorln("Failed to get absolute path for default recipe path") + recipePath = "" + } + indexPath, err = filepath.Abs(indexPath) + if err != nil { + log.WithError(err).Errorln("Failed to get absolute path for default index path") + indexPath = "" + } + + flag.StringVarP(&listenAddr, "host", "h", listenAddr, "HTTP listen address") + flag.Uint16VarP(&listenPort, "port", "p", listenPort, "HTTP listen port") + flag.StringVarP(&recipePath, "recipes", "r", recipePath, "Path to recipes") + flag.StringVarP(&indexPath, "index", "i", indexPath, "Path for search index") + flag.BoolVarP(&debug, "debug", "d", debug, "Enable debug mode") + flag.Parse() + + if debug { + log.SetLevel(log.DebugLevel) + } + + log.WithFields(log.Fields{ + "host": listenAddr, + "port": listenPort, + "recipes": recipePath, + "index": indexPath, + "debug": debug, + }).Debugln("Options received") + + log.Debugln("Creating new handler") + handler, err := NewHandler(recipePath, indexPath, log.StandardLogger()) + if err != nil { + log.WithError(err).Errorln("Failed to create new handler") + os.Exit(1) + } + + log.Debugln("Successfully created new handler") + + defer handler.Close() + + log.Debugln("Creating TCP listening port") + listener, err := net.Listen("tcp", fmt.Sprintf("%s:%d", listenAddr, listenPort)) + if err != nil { + log.WithError(err).Errorln("Failed to listen on new TCP port") + os.Exit(1) + } + + defer listener.Close() + + mux := NewMux() + + log.Debugln("Associating handler funcs with http server") + for pattern, handlerFunc := range handler.GetHandlerFuncs() { + mux.HandleFunc(pattern, handlerFunc) + } + + go func() { + log.Infoln("Waiting before opening web browser") + time.Sleep(time.Second) + url := "http://" + listener.Addr().String() + err = open.Run(url) + if err == nil { + log.Infoln("Opened", url, "in your web browser") + } else { + log.Infoln("Open", url, "in your web browser") + } + }() + + err = http.Serve(listener, mux) + if err != nil { + log.WithError(err).Fatalln("HTTP server died") + } +} diff --git a/recipe/recipe.go b/recipe/recipe.go new file mode 100644 index 0000000..627641e --- /dev/null +++ b/recipe/recipe.go @@ -0,0 +1,186 @@ +package recipe + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/tblyler/goatomic" + "github.com/tblyler/recipe-card/doc" +) + +// ValidCategoriesOrder defines keys of info in order of importance +var ValidCategoriesOrder = []string{ + "serves", + "oven temperature", + "ingredients", + "preparation", + "tips", +} + +// validCategories defines keys of Info +var validCategories = map[string]bool{ + "oven temperature": true, + "serves": true, + "ingredients": true, + "preparation": true, + "tips": true, +} + +// Recipe stores information regarding a specific recipe +type Recipe struct { + Title string `json:"title"` + Info map[string][]string `json:"info"` + // FIXME support non-docx + DocxPath string `json:"docx_path"` + ScanPaths []string `json:"scan_paths"` + Image []byte +} + +// Summary outputs a nice summary of Info +func (r *Recipe) Summary() (output string) { + for _, category := range ValidCategoriesOrder { + if info, exists := r.Info[category]; exists { + if output != "" { + // add an extra newline between categories + output += "\n" + } + + output += category + "\n" + strings.Join(info, "\n") + } + } + + return +} + +// ParseFiles for the recipe +func (r *Recipe) ParseFiles() error { + dir := filepath.Dir(r.DocxPath) + + // get a list of recipe scans + infos, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + + for _, info := range infos { + if info.IsDir() { + continue + } + + name := strings.ToLower(info.Name()) + // FIXME support non-jpeg + if !strings.HasSuffix(name, ".jpeg") && !strings.HasSuffix(name, ".jpg") { + continue + } + + r.ScanPaths = append(r.ScanPaths, filepath.Join(dir, info.Name())) + } + + sort.Strings(r.ScanPaths) + + file, err := os.Open(r.DocxPath) + if err != nil { + return err + } + + stat, err := file.Stat() + if err != nil { + return err + } + + docx, err := doc.NewDocx(file, stat.Size()) + if err != nil { + return err + } + + r.Image = docx.Image + + lines, err := docx.Text() + if err != nil { + return err + } + + r.Info = make(map[string][]string) + + titleIsNext := false + currentGroup := "" + for _, line := range lines { + if r.Title == "" { + if titleIsNext { + r.Title = line + continue + } + + if strings.Contains(strings.ToLower(line), "recipe") { + titleIsNext = true + } + + continue + } + + lowerLine := strings.ToLower(strings.Replace(line, ":", "", -1)) + if _, exists := validCategories[lowerLine]; exists { + currentGroup = lowerLine + continue + } + + // make sure a current group is set + if currentGroup == "" { + continue + } + + r.Info[currentGroup] = append(r.Info[currentGroup], line) + } + + return nil +} + +// RecipesFromPath generates Recipe instances from a path +func RecipesFromPath(dirPath string) (recipes []*Recipe, err error) { + // get the absolute path of the directory and clean it + dirPath, err = filepath.Abs(dirPath) + if err != nil { + return + } + + stat, err := os.Stat(dirPath) + if err != nil { + return + } + + if !stat.IsDir() { + return nil, fmt.Errorf("Not a directory %s", dirPath) + } + + err = filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error { + // skip directories and non-docx files + // FIXME support non-docx + if info.IsDir() || !strings.HasSuffix(strings.ToLower(path), ".docx") { + return nil + } + + recipes = append(recipes, &Recipe{ + DocxPath: path, + }) + + return nil + }) + + wg := goatomic.WorkerGroup{} + for _, recipe := range recipes { + wg.Add(1) + + go func(recipe *Recipe) { + recipe.ParseFiles() + wg.Done() + }(recipe) + } + + wg.Wait() + + return +} diff --git a/templates.go b/templates.go new file mode 100644 index 0000000..062d2d6 --- /dev/null +++ b/templates.go @@ -0,0 +1,309 @@ +package main + +import ( + "fmt" + "html/template" + "io/ioutil" + + log "github.com/sirupsen/logrus" +) + +const ( + minicss = `/*MIT License + +Copyright (c) 2016-2017 Angelos Chalaris + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE.*/ +/* https://github.com/Chalarangelo/mini.css v2.3.2*/ +html{font-size:16px}html,*{font-family:-apple-system, BlinkMacSystemFont,"Segoe UI","Roboto", "Droid Sans","Helvetica Neue", Helvetica, Arial, sans-serif;line-height:1.5;-webkit-text-size-adjust:100%}*{font-size:1rem}body{margin:0;color:#212121;background:#f8f8f8}article,aside,section,figcaption,figure,main,details,menu{display:block}summary{display:list-item}abbr[title]{border-bottom:none;text-decoration:underline}audio,video{display:inline-block}svg:not(:root){overflow:hidden}input{overflow:visible}img{max-width:100%;height:auto}dfn{font-style:italic}h1,h2,h3,h4,h5,h6{line-height:1.2em;margin:0.75rem 0.5rem;font-weight:500}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#424242;display:block;margin-top:-.25rem}h1{font-size:2rem}h2{font-size:1.6875rem}h3{font-size:1.4375rem}h4{font-size:1.1875rem}h5{font-size:1rem}h6{font-size:.8125rem}p{margin:.5rem}ol,ul{margin:.5rem;padding-left:1rem}b,strong{font-weight:700}hr{box-sizing:content-box;border:0;overflow:visible;line-height:1.25em;margin:.5rem;height:.0625rem;background:linear-gradient(to right, #bdbdbd, #8c8c8c, #bdbdbd)}blockquote{display:block;position:relative;font-style:italic;background:#eee;margin:.5rem;padding:0.5rem 0.5rem 1.5rem;border-left:.25rem solid #505050;border-radius:0 .125rem .125rem 0}blockquote:after{position:absolute;font-style:normal;font-size:.875rem;color:#505050;left:.625rem;bottom:0;content:"— " attr(cite)}code,kbd,pre,samp{font-family:monospace, monospace}code{border-radius:.125rem;background:#e6e6e6;padding:0.125rem 0.25rem}pre{overflow:auto;border-radius:0 .125rem .125rem 0;background:#e6e6e6;padding:.75rem;margin:.5rem;border-left:.25rem solid #1565c0}kbd{border-radius:.125rem;background:#0c0c0c;color:#fafafa;padding:0.125rem 0.25rem}small,sup,sub{font-size:.75em}sup{top:-.5em}sub{bottom:-.25em}sup,sub{line-height:0;position:relative;vertical-align:baseline}a{color:#0277bd;text-decoration:underline;opacity:1;transition:opacity 0.3s}a:visited{color:#01579b}a:hover,a:focus{opacity:0.75}figcaption{font-size:.8125rem;color:#424242}.container{margin:0 auto;padding:0 .75rem}.row{box-sizing:border-box;display:-webkit-box;-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-flex;display:flex;-webkit-flex:0 1 auto;flex:0 1 auto;-webkit-flex-flow:row wrap;flex-flow:row wrap}.col-sm,[class^='col-sm-'],[class^='col-sm-offset-'],.row[class*='cols-sm-']>*{box-sizing:border-box;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;padding:0 0.25rem}.col-sm,.row.cols-sm>*{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0;flex-basis:0}.col-sm-1,.row.cols-sm-1>*{max-width:8.33333%;-webkit-flex-basis:8.33333%;flex-basis:8.33333%}.col-sm-2,.row.cols-sm-2>*{max-width:16.66667%;-webkit-flex-basis:16.66667%;flex-basis:16.66667%}.col-sm-3,.row.cols-sm-3>*{max-width:25%;-webkit-flex-basis:25%;flex-basis:25%}.col-sm-4,.row.cols-sm-4>*{max-width:33.33333%;-webkit-flex-basis:33.33333%;flex-basis:33.33333%}.col-sm-5,.row.cols-sm-5>*{max-width:41.66667%;-webkit-flex-basis:41.66667%;flex-basis:41.66667%}.col-sm-6,.row.cols-sm-6>*{max-width:50%;-webkit-flex-basis:50%;flex-basis:50%}.col-sm-7,.row.cols-sm-7>*{max-width:58.33333%;-webkit-flex-basis:58.33333%;flex-basis:58.33333%}.col-sm-8,.row.cols-sm-8>*{max-width:66.66667%;-webkit-flex-basis:66.66667%;flex-basis:66.66667%}.col-sm-9,.row.cols-sm-9>*{max-width:75%;-webkit-flex-basis:75%;flex-basis:75%}.col-sm-10,.row.cols-sm-10>*{max-width:83.33333%;-webkit-flex-basis:83.33333%;flex-basis:83.33333%}.col-sm-11,.row.cols-sm-11>*{max-width:91.66667%;-webkit-flex-basis:91.66667%;flex-basis:91.66667%}.col-sm-12,.row.cols-sm-12>*{max-width:100%;-webkit-flex-basis:100%;flex-basis:100%}.col-sm-offset-0{margin-left:0}.col-sm-offset-1{margin-left:8.33333%}.col-sm-offset-2{margin-left:16.66667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.33333%}.col-sm-offset-5{margin-left:41.66667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.33333%}.col-sm-offset-8{margin-left:66.66667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.33333%}.col-sm-offset-11{margin-left:91.66667%}.col-sm-normal{-webkit-order:initial;order:initial}.col-sm-first{-webkit-order:-999;order:-999}.col-sm-last{-webkit-order:999;order:999}@media screen and (min-width: 768px){.col-md,[class^='col-md-'],[class^='col-md-offset-'],.row[class*='cols-md-']>*{box-sizing:border-box;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;padding:0 0.25rem}.col-md,.row.cols-md>*{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0;flex-basis:0}.col-md-1,.row.cols-md-1>*{max-width:8.33333%;-webkit-flex-basis:8.33333%;flex-basis:8.33333%}.col-md-2,.row.cols-md-2>*{max-width:16.66667%;-webkit-flex-basis:16.66667%;flex-basis:16.66667%}.col-md-3,.row.cols-md-3>*{max-width:25%;-webkit-flex-basis:25%;flex-basis:25%}.col-md-4,.row.cols-md-4>*{max-width:33.33333%;-webkit-flex-basis:33.33333%;flex-basis:33.33333%}.col-md-5,.row.cols-md-5>*{max-width:41.66667%;-webkit-flex-basis:41.66667%;flex-basis:41.66667%}.col-md-6,.row.cols-md-6>*{max-width:50%;-webkit-flex-basis:50%;flex-basis:50%}.col-md-7,.row.cols-md-7>*{max-width:58.33333%;-webkit-flex-basis:58.33333%;flex-basis:58.33333%}.col-md-8,.row.cols-md-8>*{max-width:66.66667%;-webkit-flex-basis:66.66667%;flex-basis:66.66667%}.col-md-9,.row.cols-md-9>*{max-width:75%;-webkit-flex-basis:75%;flex-basis:75%}.col-md-10,.row.cols-md-10>*{max-width:83.33333%;-webkit-flex-basis:83.33333%;flex-basis:83.33333%}.col-md-11,.row.cols-md-11>*{max-width:91.66667%;-webkit-flex-basis:91.66667%;flex-basis:91.66667%}.col-md-12,.row.cols-md-12>*{max-width:100%;-webkit-flex-basis:100%;flex-basis:100%}.col-md-offset-0{margin-left:0}.col-md-offset-1{margin-left:8.33333%}.col-md-offset-2{margin-left:16.66667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.33333%}.col-md-offset-5{margin-left:41.66667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.33333%}.col-md-offset-8{margin-left:66.66667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.33333%}.col-md-offset-11{margin-left:91.66667%}.col-md-normal{-webkit-order:initial;order:initial}.col-md-first{-webkit-order:-999;order:-999}.col-md-last{-webkit-order:999;order:999}}@media screen and (min-width: 1280px){.col-lg,[class^='col-lg-'],[class^='col-lg-offset-'],.row[class*='cols-lg-']>*{box-sizing:border-box;-webkit-box-flex:0;-webkit-flex:0 0 auto;flex:0 0 auto;padding:0 0.25rem}.col-lg,.row.cols-lg>*{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0;flex-basis:0}.col-lg-1,.row.cols-lg-1>*{max-width:8.33333%;-webkit-flex-basis:8.33333%;flex-basis:8.33333%}.col-lg-2,.row.cols-lg-2>*{max-width:16.66667%;-webkit-flex-basis:16.66667%;flex-basis:16.66667%}.col-lg-3,.row.cols-lg-3>*{max-width:25%;-webkit-flex-basis:25%;flex-basis:25%}.col-lg-4,.row.cols-lg-4>*{max-width:33.33333%;-webkit-flex-basis:33.33333%;flex-basis:33.33333%}.col-lg-5,.row.cols-lg-5>*{max-width:41.66667%;-webkit-flex-basis:41.66667%;flex-basis:41.66667%}.col-lg-6,.row.cols-lg-6>*{max-width:50%;-webkit-flex-basis:50%;flex-basis:50%}.col-lg-7,.row.cols-lg-7>*{max-width:58.33333%;-webkit-flex-basis:58.33333%;flex-basis:58.33333%}.col-lg-8,.row.cols-lg-8>*{max-width:66.66667%;-webkit-flex-basis:66.66667%;flex-basis:66.66667%}.col-lg-9,.row.cols-lg-9>*{max-width:75%;-webkit-flex-basis:75%;flex-basis:75%}.col-lg-10,.row.cols-lg-10>*{max-width:83.33333%;-webkit-flex-basis:83.33333%;flex-basis:83.33333%}.col-lg-11,.row.cols-lg-11>*{max-width:91.66667%;-webkit-flex-basis:91.66667%;flex-basis:91.66667%}.col-lg-12,.row.cols-lg-12>*{max-width:100%;-webkit-flex-basis:100%;flex-basis:100%}.col-lg-offset-0{margin-left:0}.col-lg-offset-1{margin-left:8.33333%}.col-lg-offset-2{margin-left:16.66667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.33333%}.col-lg-offset-5{margin-left:41.66667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.33333%}.col-lg-offset-8{margin-left:66.66667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.33333%}.col-lg-offset-11{margin-left:91.66667%}.col-lg-normal{-webkit-order:initial;order:initial}.col-lg-first{-webkit-order:-999;order:-999}.col-lg-last{-webkit-order:999;order:999}}form{background:#eee;border:.0625rem solid #c9c9c9;margin:.5rem;padding:0.75rem 0.5rem 1.125rem}fieldset{border:.0625rem solid #d0d0d0;border-radius:.125rem;margin:.125rem;padding:.5rem}legend{box-sizing:border-box;display:table;max-width:100%;white-space:normal;font-weight:700;font-size:.875rem;padding:0.125rem 0.25rem}label{padding:0.25rem 0.5rem}.input-group{display:inline-block}.input-group.fluid{display:-webkit-box;-webkit-box-pack:justify;display:-webkit-flex;display:flex;-webkit-align-items:center;align-items:center;-webkit-justify-content:center;justify-content:center}.input-group.fluid>input{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0px;flex-basis:0px}@media screen and (max-width: 767px){.input-group.fluid{-webkit-box-orient:vertical;-webkit-align-items:stretch;align-items:stretch;-webkit-flex-direction:column;flex-direction:column}}.input-group.vertical{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-pack:justify;display:-webkit-flex;display:flex;-webkit-flex-direction:column;flex-direction:column;-webkit-align-items:stretch;align-items:stretch;-webkit-justify-content:center;justify-content:center}.input-group.vertical>input{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0px;flex-basis:0px}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}textarea{overflow:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}input:not([type]),[type="text"],[type="email"],[type="number"],[type="search"],[type="password"],[type="url"],[type="tel"],textarea,select{box-sizing:border-box;background:#fafafa;color:#212121;border:.0625rem solid #c9c9c9;border-radius:.125rem;margin:.25rem;padding:0.5rem 0.75rem}input:not([type="button"]):not([type="submit"]):not([type="reset"]):hover,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus,textarea:hover,textarea:focus,select:hover,select:focus{border-color:#0288d1;box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"]):invalid,input:not([type="button"]):not([type="submit"]):not([type="reset"]):focus:invalid,textarea:invalid,textarea:focus:invalid,select:invalid,select:focus:invalid{border-color:#d32f2f;box-shadow:none}input:not([type="button"]):not([type="submit"]):not([type="reset"])[readonly],textarea[readonly],select[readonly]{background:#e5e5e5;border-color:#c9c9c9}select{padding-right:1.5rem;background:url('data:image/svg+xml, ') no-repeat right;background-color:#fafafa;-webkit-appearance:none;-moz-appearance:none;appearance:none}select[readonly]{background-color:#e5e5e5}::-webkit-input-placeholder{opacity:1;color:#616161}::-moz-placeholder{opacity:1;color:#616161}::-ms-placeholder{opacity:1;color:#616161}::placeholder{opacity:1;color:#616161}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button{overflow:visible;text-transform:none}button,[type="button"],[type="submit"],[type="reset"],a.button,label.button,.button,a[role="button"],label[role="button"],[role="button"]{display:inline-block;background:rgba(220,220,220,0.75);color:#212121;border:0;border-radius:.125rem;padding:0.5rem 0.75rem;margin:.5rem;text-decoration:none;transition:background 0.3s;cursor:pointer}button:hover,button:focus,[type="button"]:hover,[type="button"]:focus,[type="submit"]:hover,[type="submit"]:focus,[type="reset"]:hover,[type="reset"]:focus,a.button:hover,a.button:focus,label.button:hover,label.button:focus,.button:hover,.button:focus,a[role="button"]:hover,a[role="button"]:focus,label[role="button"]:hover,label[role="button"]:focus,[role="button"]:hover,[role="button"]:focus{background:#dcdcdc;opacity:1}input:disabled,input[disabled],textarea:disabled,textarea[disabled],select:disabled,select[disabled],button:disabled,button[disabled],.button:disabled,.button[disabled],[role="button"]:disabled,[role="button"][disabled]{cursor:not-allowed;opacity:.75}input[type="file"]{border:0;height:1px;width:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}.button-group{display:-webkit-box;display:-webkit-flex;display:flex;border:.0625rem solid #bdbdbd;border-radius:.125rem;margin:.5rem}.button-group>button,.button-group [type="button"],.button-group>[type="submit"],.button-group>[type="reset"],.button-group>.button,.button-group>[role="button"]{margin:0;-webkit-box-flex:1;max-width:100%;-webkit-flex:1 1 auto;flex:1 1 auto;text-align:center;border:0;border-radius:0}.button-group>:not(:first-child){border-left:.0625rem solid #bdbdbd}@media screen and (max-width: 767px){.button-group{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}.button-group>:not(:first-child){border:0;border-top:.0625rem solid #bdbdbd}}[type="checkbox"],[type="radio"]{height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}.input-group [type="checkbox"]+label,.input-group [type="radio"]+label{position:relative;margin-left:1.25rem;cursor:pointer}.input-group [type="checkbox"]+label:before,.input-group [type="radio"]+label:before{display:inline-block;position:absolute;bottom:.375rem;left:0;width:1rem;height:1rem;content:'';border:.0625rem solid #bdbdbd;border-radius:.125rem;background:#fafafa;color:#212121;margin-left:-1.25rem}.input-group [type="checkbox"]+label:hover:before,.input-group [type="checkbox"]+label:focus:before,.input-group [type="radio"]+label:hover:before,.input-group [type="radio"]+label:focus:before{border-color:#0288d1}.input-group [type="checkbox"]:focus+label:before,.input-group [type="radio"]:focus+label:before{border-color:#0288d1}.input-group [type="radio"]+label:before,.input-group [type="radio"]+label:after{border-radius:50%}.input-group [type="checkbox"][disabled]+label,.input-group [type="radio"][disabled]+label,.input-group [type="checkbox"]:disabled+label,.input-group [type="radio"]:disabled+label{cursor:not-allowed}.input-group [type="checkbox"][disabled]+label:before,.input-group [type="checkbox"][disabled]+label:after,.input-group [type="radio"][disabled]+label:before,.input-group [type="radio"][disabled]+label:after,.input-group [type="checkbox"]:disabled+label:before,.input-group [type="checkbox"]:disabled+label:after,.input-group [type="radio"]:disabled+label:before,.input-group [type="radio"]:disabled+label:after{opacity:.75}.input-group [type="checkbox"]:checked+label:after,.input-group [type="radio"]:checked+label:after{position:absolute;background:#212121;content:'';margin-left:-1.25rem;bottom:.625rem;left:.25rem;width:.625rem;height:.625rem}.input-group [type="checkbox"]+label.switch:before,.input-group [type="radio"]+label.switch:before{bottom:.5rem;width:1.75rem;height:.875rem;border:0;border-radius:.5rem;background:#c9c9c9;margin-left:-2.375rem}.input-group [type="checkbox"]+label.switch:after,.input-group [type="radio"]+label.switch:after{display:inline-block;content:'';position:absolute;left:0;width:1.25rem;height:1.25rem;background:#e0e0e0;border-radius:100%;bottom:.3125rem;margin-left:-3rem;transition:left 0.3s}.input-group [type="checkbox"]:checked+label.switch:before,.input-group [type="radio"]:checked+label.switch:before{background:#dcdcdc}.input-group [type="checkbox"]:checked+label.switch:after,.input-group [type="radio"]:checked+label.switch:after{left:1.75rem;width:1.25rem;height:1.25rem;bottom:.3125rem;margin-left:-3rem;background:#0277bd}button.primary,[type="button"].primary,[type="submit"].primary,[type="reset"].primary,.button.primary,[role="button"].primary{background:rgba(2,119,189,0.9);color:#fafafa}button.primary:hover,button.primary:focus,[type="button"].primary:hover,[type="button"].primary:focus,[type="submit"].primary:hover,[type="submit"].primary:focus,[type="reset"].primary:hover,[type="reset"].primary:focus,.button.primary:hover,.button.primary:focus,[role="button"].primary:hover,[role="button"].primary:focus{background:#0277bd}button.secondary,[type="button"].secondary,[type="submit"].secondary,[type="reset"].secondary,.button.secondary,[role="button"].secondary{background:rgba(198,40,40,0.9);color:#fafafa}button.secondary:hover,button.secondary:focus,[type="button"].secondary:hover,[type="button"].secondary:focus,[type="submit"].secondary:hover,[type="submit"].secondary:focus,[type="reset"].secondary:hover,[type="reset"].secondary:focus,.button.secondary:hover,.button.secondary:focus,[role="button"].secondary:hover,[role="button"].secondary:focus{background:#c62828}button.tertiary,[type="button"].tertiary,[type="submit"].tertiary,[type="reset"].tertiary,.button.tertiary,[role="button"].tertiary{background:rgba(95,145,51,0.9);color:#fafafa}button.tertiary:hover,button.tertiary:focus,[type="button"].tertiary:hover,[type="button"].tertiary:focus,[type="submit"].tertiary:hover,[type="submit"].tertiary:focus,[type="reset"].tertiary:hover,[type="reset"].tertiary:focus,.button.tertiary:hover,.button.tertiary:focus,[role="button"].tertiary:hover,[role="button"].tertiary:focus{background:#5f9133}button.inverse,[type="button"].inverse,[type="submit"].inverse,[type="reset"].inverse,.button.inverse,[role="button"].inverse{background:rgba(12,12,12,0.9);color:#fafafa}button.inverse:hover,button.inverse:focus,[type="button"].inverse:hover,[type="button"].inverse:focus,[type="submit"].inverse:hover,[type="submit"].inverse:focus,[type="reset"].inverse:hover,[type="reset"].inverse:focus,.button.inverse:hover,.button.inverse:focus,[role="button"].inverse:hover,[role="button"].inverse:focus{background:#0c0c0c}button.small,[type="button"].small,[type="submit"].small,[type="reset"].small,.button.small,[role="button"].small{border-radius:.0625rem;padding:0.25rem 0.375rem}button.large,[type="button"].large,[type="submit"].large,[type="reset"].large,.button.large,[role="button"].large{border-radius:.25rem;padding:0.75rem 1.125rem}header{display:block;height:2.75rem;background:#12171a;color:#f5f5f5;padding:0.125rem 0.5rem;white-space:nowrap;overflow-x:auto;overflow-y:hidden}header .logo{color:#f5f5f5;font-size:1.75rem;line-height:1.3125em;margin:0.0625rem 0.375rem 0.0625rem 0.0625rem;transition:opacity 0.3s}header button,header [type="button"],header a.button,header label.button,header .button,header a[role="button"],header label[role="button"],header [role="button"]{background:#12171a;color:#f5f5f5;vertical-align:top;margin:0.125rem 0}header button:hover,header button:focus,header [type="button"]:hover,header [type="button"]:focus,header a.button:hover,header a.button:focus,header label.button:hover,header label.button:focus,header .button:hover,header .button:focus,header a[role="button"]:hover,header a[role="button"]:focus,header label[role="button"]:hover,header label[role="button"]:focus,header [role="button"]:hover,header [role="button"]:focus{background:#20292e}header .logo,header a.button,header a[role="button"]{text-decoration:none}header.row{box-sizing:content-box}nav{display:block;background:#eceff1;border:.0625rem solid #c9c9c9;margin:.5rem;padding:0.75rem 1rem}nav a,nav a:visited{display:block;color:#145caf;text-decoration:none}nav .sublink-1{padding-left:1rem;position:relative}nav .sublink-1:before{position:absolute;left:.1875rem;top:-.0625rem;content:'';height:100%;border:.0625rem solid #bdbdbd;border-left:0}nav .sublink-2{padding-left:2rem;position:relative}nav .sublink-2:before{position:absolute;left:.1875rem;top:-.0625rem;content:'';height:100%;border:.0625rem solid #bdbdbd;border-left:0}footer{display:block;background:#192024;color:#f5f5f5;margin:1rem 0 0;padding:1.5rem 0.5rem 0.75rem;font-size:.875rem}footer a,footer a:visited{color:#0288d1}header.sticky,footer.sticky{position:-webkit-sticky;position:sticky;z-index:1101}header.sticky{top:0}footer.sticky{bottom:0}.drawer-toggle:before{position:relative;top:.4375rem;font-family:sans-serif;font-size:2.5rem;line-height:.125;content:'\2261'}.drawer{display:block;box-sizing:border-box;position:fixed;top:0;width:320px;height:100vh;overflow-y:auto;background:#eceff1;border:.0625rem solid #c9c9c9;margin:0;z-index:1110}.drawer:not(.right){left:-320px;transition:left 0.3s}.drawer.right{right:-320px;transition:right 0.3s}.drawer .close{position:absolute;top:.75rem;right:.25rem;z-index:1111;padding:0}@media screen and (max-width: 320px){.drawer{width:100%}}@media screen and (min-width: 768px){.drawer:not(.persistent){position:static;height:100%;z-index:1100}.drawer:not(.persistent) .close{display:none}.drawer-toggle:not(.persistent){display:none}}:checked+.drawer:not(.right){left:0}:checked+.drawer.right{right:0}table{border-collapse:separate;border-spacing:0;border:.0625rem solid #c9c9c9;margin:0 auto}table caption{font-size:1.5rem;margin:.5rem}table tr{padding:.5rem}table th,table td{padding:.625rem;border-left:.0625rem solid #c9c9c9;border-top:.0625rem solid #c9c9c9}table td{background:#fafafa}table thead th{border-top:0}table th{background:#e6e6e6}table th:first-child,table td:first-child{border-left:0}@media screen and (max-width: 767px){table:not(.preset){border-collapse:collapse;border:0;width:100%}table:not(.preset) thead,table:not(.preset) th{border:0;height:1px;width:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%)}table:not(.preset) tr{display:block;border:.0625rem solid #c9c9c9;background:#fafafa;margin-bottom:.625rem}table:not(.preset) td{display:block;border:0;border-bottom:.0625rem solid #c9c9c9;text-align:right}table:not(.preset) td:before{content:attr(data-label);float:left;font-weight:700}table:not(.preset) td:last-child{border-bottom:0}}@media screen and (min-width: 768px){table.horizontal,table.scrollable{display:-webkit-box;-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-flex;display:flex;-webkit-flex:0 1 auto;flex:0 1 auto;-webkit-flex-flow:row wrap;flex-flow:row wrap;padding:.5rem}table.horizontal caption,table.scrollable caption{-webkit-box-flex:1;max-width:100%;-webkit-flex:0 0 100%;flex:0 0 100%}table.horizontal thead,table.horizontal tbody,table.scrollable thead,table.scrollable tbody{display:-webkit-box;-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-flex;display:flex}table.horizontal thead,table.scrollable thead{z-index:999}table.horizontal tr,table.scrollable tr{display:-webkit-box;display:-webkit-flex;display:flex}table.horizontal thead,table.horizontal tbody{-webkit-flex-flow:row nowrap;flex-flow:row nowrap}table.horizontal tbody{overflow:auto;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;-webkit-flex:1 0 0;flex:1 0 0}table.horizontal tr{-webkit-box-flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-flex:1 0 auto;flex:1 0 auto}table.horizontal th,table.horizontal td{width:100%;border:.0625rem solid #c9c9c9}table.horizontal th:not(:first-child),table.horizontal td:not(:first-child){border-top:0}table.horizontal th{text-align:right}table.horizontal thead tr:first-child{padding-left:0}table.horizontal tbody tr:first-child>td{padding-left:1.25rem}table.scrollable{overflow:auto;max-height:400px;border:0;padding-top:0}table.scrollable thead,table.scrollable tbody{-webkit-box-flex:1;max-width:100%;-webkit-flex-flow:row wrap;flex-flow:row wrap;-webkit-flex:0 0 100%;flex:0 0 100%;border:.0625rem solid #c9c9c9}table.scrollable tbody{border-top:0;margin-top:-0.0625rem}table.scrollable tr{-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;flex-flow:row wrap;-webkit-flex:0 0 100%;flex:0 0 100%;padding:0}table.scrollable th,table.scrollable td{-webkit-box-flex:1;-webkit-flex:1 0 0%;flex:1 0 0%;overflow:hidden;text-overflow:ellipsis}table.scrollable thead{position:sticky;top:0}}@media screen and (max-width: 767px){table.horizontal.preset,table.scrollable.preset{display:-webkit-box;-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-flex;display:flex;-webkit-flex:0 1 auto;flex:0 1 auto;-webkit-flex-flow:row wrap;flex-flow:row wrap;padding:.5rem}table.horizontal.preset caption,table.scrollable.preset caption{-webkit-box-flex:1;max-width:100%;-webkit-flex:0 0 100%;flex:0 0 100%}table.horizontal.preset thead,table.horizontal.preset tbody,table.scrollable.preset thead,table.scrollable.preset tbody{display:-webkit-box;-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;display:-webkit-flex;display:flex}table.horizontal.preset thead,table.scrollable.preset thead{z-index:999}table.horizontal.preset tr,table.scrollable.preset tr{display:-webkit-box;display:-webkit-flex;display:flex}table.horizontal.preset thead,table.horizontal.preset tbody{-webkit-flex-flow:row nowrap;flex-flow:row nowrap}table.horizontal.preset tbody{overflow:auto;-webkit-box-pack:justify;-webkit-justify-content:space-between;justify-content:space-between;-webkit-flex:1 0 0;flex:1 0 0}table.horizontal.preset tr{-webkit-box-flex:1;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;flex-direction:column;-webkit-flex:1 0 auto;flex:1 0 auto}table.horizontal.preset th,table.horizontal.preset td{width:100%;border:.0625rem solid #c9c9c9}table.horizontal.preset th:not(:first-child),table.horizontal.preset td:not(:first-child){border-top:0}table.horizontal.preset th{text-align:right}table.horizontal.preset thead tr:first-child{padding-left:0}table.horizontal.preset tbody tr:first-child>td{padding-left:1.25rem}table.scrollable.preset{overflow:auto;max-height:400px;border:0;padding-top:0}table.scrollable.preset thead,table.scrollable.preset tbody{-webkit-box-flex:1;max-width:100%;-webkit-flex-flow:row wrap;flex-flow:row wrap;-webkit-flex:0 0 100%;flex:0 0 100%;border:.0625rem solid #c9c9c9}table.scrollable.preset tbody{border-top:0;margin-top:-0.0625rem}table.scrollable.preset tr{-webkit-box-flex:0;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-flow:row wrap;flex-flow:row wrap;-webkit-flex:0 0 100%;flex:0 0 100%;padding:0}table.scrollable.preset th,table.scrollable.preset td{-webkit-box-flex:1;-webkit-flex:1 0 0%;flex:1 0 0%;overflow:hidden;text-overflow:ellipsis}table.scrollable.preset thead{position:sticky;top:0}}table.striped tr:nth-of-type(2n)>td{background:#e5e5e5}@media screen and (max-width: 767px){table.striped:not(.preset) tr:nth-of-type(2n){background:#e5e5e5}}.card{display:-webkit-box;-webkit-box-orient:vertical;-webkit-box-pack:justify;-webkit-box-align:center;display:-webkit-flex;display:flex;-webkit-flex-direction:column;flex-direction:column;-webkit-justify-content:space-between;justify-content:space-between;-webkit-align-self:center;align-self:center;position:relative;width:100%;background:#fafafa;border:.0625rem solid #acacac;margin:.5rem;overflow:hidden}.card>.section{box-sizing:border-box;margin:0;border:0;border-radius:0;border-bottom:.0625rem solid #c9c9c9;padding:.5rem;width:100%}.card>.section.media{height:200px;padding:0;-o-object-fit:cover;object-fit:cover}.card>.section:last-child{border-bottom:0}@media screen and (min-width: 320px){.card{max-width:320px}}@media screen and (min-width: 480px){.card.large{max-width:480px}}@media screen and (min-width: 240px){.card.small{max-width:240px}}.card.fluid{max-width:100%;width:auto}.card.warning{background:#ffca28;border:.0625rem solid #e8b825}.card.warning>.section{border-bottom:.0625rem solid #e8b825}.card.warning>.section:last-child{border-bottom:0}.card.error{background:#b71c1c;color:#fafafa;border:.0625rem solid #a71a1a}.card.error>.section{border-bottom:.0625rem solid #a71a1a}.card.error>.section:last-child{border-bottom:0}.card>.section.dark{background:#e0e0e0}.card>.section.darker{background:#bdbdbd}.card>.section.double-padded{padding:.75rem}.tabs{width:100%;opacity:1;display:-webkit-box;-webkit-box-pack:justify;display:-webkit-flex;display:flex;-webkit-justify-content:space-between;justify-content:space-between;-webkit-flex-wrap:wrap;flex-wrap:wrap}.tabs>label{-webkit-box-flex:1;-webkit-flex-grow:1;flex-grow:1;-webkit-order:1;order:1;display:inline-block;height:1.5rem;cursor:pointer;transition:background 0.3s;background:#e6e6e6;border:.0625rem solid #bdbdbd;padding:.75rem}.tabs>label:hover,.tabs>label:focus{background:rgba(230,230,230,0.8)}.tabs>[type="radio"],.tabs>[type="checkbox"]{display:none;visibility:hidden}.tabs>label+div{-webkit-flex-basis:auto;flex-basis:auto;-webkit-order:2;order:2;height:1px;width:1px;margin:-1px;overflow:hidden;position:absolute;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);-webkit-transform:scaleY(0);transform:scaleY(0);-webkit-transform-origin:top;transform-origin:top;transition:-webkit-transform 0.3s, + transform 0.3s}.tabs>label:not(:first-of-type){border-left:0}.tabs>:checked+label{background:#eee;border-bottom-color:#0277bd}.tabs>:checked+label:hover,.tabs>:checked+label:focus{background:rgba(238,238,238,0.8)}.tabs>:checked+label+div{box-sizing:border-box;position:relative;height:400px;width:100%;overflow:auto;margin:0;-webkit-transform:scaleY(1);transform:scaleY(1);background:#fafafa;border:.0625rem solid #bdbdbd;border-top:0;padding:.5rem;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tabs.stacked{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}.tabs.stacked>label{-webkit-order:initial;order:initial}.tabs.stacked>:checked+label{border:.0625rem solid #bdbdbd}.tabs.stacked>label+div{-webkit-order:initial;order:initial;-webkit-transform-origin:top;transform-origin:top}.tabs.stacked>label:not(:first-of-type){border:.0625rem solid #bdbdbd;border-top:0}.tabs.stacked>:checked+label+div{height:auto}@media screen and (max-width: 767px){.tabs{-webkit-box-orient:vertical;-webkit-flex-direction:column;flex-direction:column}.tabs>label{-webkit-order:initial;order:initial}.tabs>:checked+label{border:.0625rem solid #bdbdbd}.tabs>label+div{-webkit-order:initial;order:initial}.tabs>label:not(:first-of-type){border:.0625rem solid #bdbdbd;border-top:0}}mark{background:#0277bd;color:#fafafa;font-size:.9375em;line-height:1em;border-radius:.125rem;padding:0.125em 0.25em}mark.inline-block{display:inline-block}.toast{position:fixed;background:#424242;bottom:1.5rem;left:50%;transform:translate(-50%, -50%);color:#fafafa;border-radius:2rem;padding:0.75rem 1.5rem;z-index:1111}.tooltip{position:relative;display:inline-block}.tooltip:before,.tooltip:after{position:absolute;opacity:0;clip:rect(0 0 0 0);-webkit-clip-path:inset(100%);clip-path:inset(100%);transition:all 0.3s;z-index:1010;left:50%}.tooltip:not(.bottom):before,.tooltip:not(.bottom):after{bottom:75%}.tooltip.bottom:before,.tooltip.bottom:after{top:75%}.tooltip:hover:before,.tooltip:hover:after,.tooltip:focus:before,.tooltip:focus:after{opacity:1;clip:auto;-webkit-clip-path:inset(0%);clip-path:inset(0%)}.tooltip:before{content:'';background:transparent;border:.5rem solid transparent;left:50%;left:calc(50% - .5rem)}.tooltip:not(.bottom):before{border-top-color:#212121}.tooltip.bottom:before{border-bottom-color:#212121}.tooltip:after{content:attr(aria-label);background:#212121;border-radius:.125rem;color:#fafafa;padding:.5rem;white-space:nowrap;-webkit-transform:translateX(-50%);transform:translateX(-50%)}.tooltip:not(.bottom):after{margin-bottom:1rem}.tooltip.bottom:after{margin-top:1rem}.modal{position:fixed;top:0;left:0;display:none;width:100vw;height:100vh;background:rgba(0,0,0,0.45)}.modal .card{margin:0 auto;max-height:50vh;overflow:auto}.modal .card .close{position:absolute;top:.75rem;right:.25rem;padding:0}:checked+.modal{display:-webkit-box;-webkit-box-flex:0;display:-webkit-flex;display:flex;-webkit-flex:0 1 auto;flex:0 1 auto;z-index:1200}:checked+.modal .card .close{z-index:1211}mark.secondary{background:#e53935}mark.tertiary{background:#689f38}mark.tag{border-radius:200px;padding:0.25em 0.5em}mark.inline-block{font-size:1em;line-height:1.375em;padding:.375em}.toast.small{border-radius:1.5rem;padding:0.5rem 1rem}.toast.large{border-radius:3rem;padding:1.125rem 2.25rem}progress{display:block;vertical-align:baseline;-webkit-appearance:none;-moz-appearance:none;appearance:none;height:.625rem;width:90%;width:calc(100% - 1rem);margin:.5rem .5rem;border:0;border-radius:.125rem;background:#e0e0e0;color:#0277bd}progress::-webkit-progress-value{background:#0277bd;border-top-left-radius:.125rem;border-bottom-left-radius:.125rem}progress::-webkit-progress-bar{background:#e0e0e0}progress::-moz-progress-bar{background:#0277bd;border-top-left-radius:.125rem;border-bottom-left-radius:.125rem}progress[value="1000"]::-webkit-progress-value{border-radius:.125rem}progress[value="1000"]::-moz-progress-bar{border-radius:.125rem}@-webkit-keyframes spinner-donut-anim{0%{-webkit-transform:rotate(0deg)}100%{-webkit-transform:rotate(360deg)}}@keyframes spinner-donut-anim{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}.spinner-donut{display:inline-block;border:.25rem solid #e3f2fd;border-left:.25rem solid #1565c0;border-radius:50%;width:1.25rem;height:1.25rem;-webkit-animation:spinner-donut-anim 1.2s linear infinite;animation:spinner-donut-anim 1.2s linear infinite}progress.inline{display:inline-block;vertical-align:middle;width:60%}progress.secondary{color:#e53935}progress.secondary::-webkit-progress-value{background:#e53935}progress.secondary::-moz-progress-bar{background:#e53935}progress.tertiary{color:#689f38}progress.tertiary::-webkit-progress-value{background:#689f38}progress.tertiary::-moz-progress-bar{background:#689f38}.spinner-donut.secondary{border:.25rem solid #ffebee;border-left:.25rem solid #c62828}.spinner-donut.tertiary{border:.25rem solid #e8f5e9;border-left:.25rem solid #2e7d32}.spinner-donut.large{border-width:.375rem;width:2rem;height:2rem}.hidden{display:none !important}.visually-hidden{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}ul.breadcrumbs{display:-webkit-box;display:-webkit-flex;display:flex;list-style:none;margin:.5rem;padding:0}ul.breadcrumbs li{-webkit-box-flex:1;max-width:100%;-webkit-flex-grow:1;flex-grow:1;-webkit-flex-basis:0;flex-basis:0;position:relative;text-align:center;background:#e6e6e6;height:2rem;line-height:2rem;margin-right:1.125rem}ul.breadcrumbs li:before,ul.breadcrumbs li:after{content:"";position:absolute;top:0;width:0;height:0;border:0 solid #e6e6e6;border-width:1rem 1rem}ul.breadcrumbs li:before{left:-1rem;border-left-color:transparent}ul.breadcrumbs li:after{left:100%;border-color:transparent;border-left-color:#e6e6e6}ul.breadcrumbs li:first-child:before{border:0}ul.breadcrumbs li:last-child{margin-right:0}ul.breadcrumbs li:last-child:after{border:0}.close{display:inline-block;width:1.5rem;font-family:sans-serif;font-size:1.5rem;line-height:1;font-weight:700;border-radius:2rem;background:rgba(230,230,230,0);vertical-align:top;cursor:pointer;transition:background 0.3s}.close:hover,.close:focus{background:#e6e6e6}.close:before{content:"\00D7";display:block;text-align:center}.bordered{border:1px solid rgba(0,0,0,0.25) !important}.rounded{border-radius:.125rem !important}.circular{border-radius:50% !important}.responsive-margin{margin:.25rem !important}@media screen and (min-width: 768px){.responsive-margin{margin:.375rem !important}}@media screen and (min-width: 1280px){.responsive-margin{margin:.5rem !important}}.responsive-padding{padding:0.125rem 0.25rem !important}@media screen and (min-width: 768px){.responsive-padding{padding:0.25rem 0.375rem !important}}@media screen and (min-width: 1280px){.responsive-padding{padding:0.375rem 0.5rem !important}}.shadowed{box-shadow:0 .25rem .25rem 0 rgba(0,0,0,0.125),0 .125rem .125rem -.125rem rgba(0,0,0,0.25) !important}@media screen and (max-width: 767px){.hidden-sm{display:none !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.hidden-md{display:none !important}}@media screen and (min-width: 1280px){.hidden-lg{display:none !important}}@media screen and (max-width: 767px){.visually-hidden-sm{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 768px) and (max-width: 1279px){.visually-hidden-md{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}@media screen and (min-width: 1280px){.visually-hidden-lg{position:absolute !important;width:1px !important;height:1px !important;margin:-1px !important;border:0 !important;padding:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(100%) !important;clip-path:inset(100%) !important;overflow:hidden !important}}` + + maincss = `h1, h2, h3, h4, h5, h6 { + text-transform: capitalize; +} +img { + image-orientation: from-image; +} +.recipeCardTitle { + text-decoration: none; + color: unset; +} +.recipeCardDesc { + max-height: 200px; + overflow: auto; +} +#recipeImages { + padding-top: 20px; + text-align: center; +} +.recipeImage { + cursor: pointer; +} +.imageModal { + margin-top: 15%; + max-width: 50%; + max-height: 50%; + text-align: right; +} +.recipeCardImageSection { + text-align: center; +}` + + templateHeader = `{{ define "header" }} + + + + {{ .PageTitle }} + + + + + + +
+ + Recipes +
+{{ end }}` + + templateFooter = `{{ define "footer" }} + + +{{ end }}` + + templateSearchForm = `{{ define "searchform" }} +
+
+
+
+
+ +
+
+
+{{ end }}` + + templateIndex = `{{ define "index" }} +{{ template "header" . }} +{{ template "searchform" . }} +{{ template "footer" . }} +{{ end }}` + + templateRecipeCard = `{{ define "recipecard" }} +
+ {{ if .StockImage }} +
+ + Recipe Image + +
+ {{ end }} +
+

{{ .ID }}

+
+
+ {{ .Description }} +
+
+{{ end }}` + + templateRecipeCards = `{{ define "recipecards" }} +
+ {{ range . }} + {{ template "recipecard" . }} + {{ end }} +
+{{ end }}` + + templateSearch = `{{ define "search" }} +{{ template "header" . }} +{{ template "searchform" . }} +{{ if .Recipes }} +{{ template "recipecards" .Recipes }} +{{ else }} +

Unable to find recipes. :(

+{{ end }} +{{ template "footer" . }} +{{ end }}` + + templateRecipes = `{{ define "recipes" }} +{{ template "header" . }} +{{ if .Recipes }} +{{ template "recipecards" .Recipes }} +{{ else }} +

No recipes found :(

+{{ end }} +{{ template "footer" . }} +{{ end }}` + + templateRecipe = `{{ define "recipe" }} +{{ template "header" . }} +{{ with index .Recipes 0 }} +
+
+ {{ if or .StockImage .Images }} +
+ {{ end }} + {{ if .StockImage }} + Stock image + {{ end }} + {{ range $index, $results := .Images }} + + + + {{ end }} + {{ if or .StockImage .Images }} +
+ {{ end }} +
+

{{ .ID }}

+
+ {{ .Description }} +
+
+
+{{ end }} +{{ template "footer" . }} +{{ end }}` +) + +// TemplateData defines default template data to pass to every template +type TemplateData struct { + // title of the page + PageTitle string + // previous value used for searching + SearchValue string + // list of recipes to display + Recipes []*TemplateRecipe +} + +// TemplateRecipe used for all recipes whether it is an aggregate or a singular recipe +type TemplateRecipe struct { + // title of the recipe + ID string + // description of the recipe in HTML + Description template.HTML + // relative URL to the recipe page itself + URL string + // relative URL to the recipe docx file + DocxURL string + // stock image relative path for the recipe + StockImage string + // card scan images relative paths for the recipe + Images []string +} + +// NewTemplate creates a new template instance with all recipe-card related templates parsed +func NewTemplate(logger *log.Logger) (tmpl *template.Template, err error) { + tmpl = template.New("recipe-card") + + if logger == nil { + logger = log.New() + logger.Out = ioutil.Discard + } + + logger.Debugln("Parsing header template") + _, err = tmpl.Parse(templateHeader) + if err != nil { + err = fmt.Errorf("templateHeader: %s", err) + return + } + + logger.Debugln("Finished parsing header template") + + logger.Debugln("Parsing footer template") + _, err = tmpl.Parse(templateFooter) + if err != nil { + err = fmt.Errorf("templateFooter: %s", err) + return + } + + logger.Debugln("Finished parsing footer template") + + logger.Debugln("Parsing recipe card template") + _, err = tmpl.Parse(templateRecipeCard) + if err != nil { + err = fmt.Errorf("templateRecipeCard: %s", err) + return + } + + logger.Debugln("Finished parsing recipe card template") + + logger.Debugln("Parsing recipe cards template") + _, err = tmpl.Parse(templateRecipeCards) + if err != nil { + err = fmt.Errorf("templateRecipeCards: %s", err) + return + } + + logger.Debugln("Finished parsing recipe cards template") + + logger.Debugln("Parsing search form template") + _, err = tmpl.Parse(templateSearchForm) + if err != nil { + err = fmt.Errorf("templateSearchForm: %s", err) + return + } + + logger.Debugln("Finished parsing search form template") + + logger.Debugln("Parsing index template") + _, err = tmpl.Parse(templateIndex) + if err != nil { + err = fmt.Errorf("templateIndex: %s", err) + return + } + + logger.Debugln("Finished parsing index template") + + logger.Debugln("Parsing recipes template") + _, err = tmpl.Parse(templateRecipes) + if err != nil { + err = fmt.Errorf("templateRecipes: %s", err) + return + } + + logger.Debugln("Finished parsing recipes template") + + logger.Debugln("Parsing recipe template") + _, err = tmpl.Parse(templateRecipe) + if err != nil { + err = fmt.Errorf("templateRecipe: %s", err) + return + } + + logger.Debugln("Finished parsing recipe template") + + logger.Debugln("Parsing search template") + _, err = tmpl.Parse(templateSearch) + if err != nil { + err = fmt.Errorf("templateSearch: %s", err) + } + + logger.Debugln("Finished parsing search template") + + return +}