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 }}
+
+
+
+
+
+
+
+{{ end }}`
+
+ templateFooter = `{{ define "footer" }}
+
+
+{{ end }}`
+
+ templateSearchForm = `{{ define "searchform" }}
+
+{{ end }}`
+
+ templateIndex = `{{ define "index" }}
+{{ template "header" . }}
+{{ template "searchform" . }}
+{{ template "footer" . }}
+{{ end }}`
+
+ templateRecipeCard = `{{ define "recipecard" }}
+
+ {{ if .StockImage }}
+
+ {{ end }}
+
+
+ {{ .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 }}
+
+ {{ end }}
+ {{ range $index, $results := .Images }}
+
+
+
+
+
+
+
+
+ {{ end }}
+ {{ if or .StockImage .Images }}
+
+ {{ end }}
+
+
+
+{{ 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
+}