Initial dirty but working commit

This commit is contained in:
Tony Blyler 2017-07-08 01:17:01 -04:00
parent 045b274107
commit 28b18ec679
7 changed files with 1927 additions and 0 deletions

13
README.md Normal file
View file

@ -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.

131
doc/docx.go Normal file
View file

@ -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
}
}
}

610
doc/docx_test.go Normal file
View file

@ -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])
}
}
}

534
handler.go Normal file
View file

@ -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 += "<p>" + html.EscapeString(infoLine) + "</p>"
}
tmplRecipe.Description += template.HTML(fmt.Sprintf(
"<h3>%s</h3>%s",
html.EscapeString(category),
description,
))
}
}
return tmplRecipe
}

144
main.go Normal file
View file

@ -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")
}
}

186
recipe/recipe.go Normal file
View file

@ -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
}

309
templates.go Normal file

File diff suppressed because one or more lines are too long