commit fa2662369b6672cd82e820fe544b0a89c7ceebf6 Author: Carl Kittelberger Date: Thu Feb 9 15:13:03 2017 +0100 Initial commit. diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..2f02d46 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true + +[*.json] +indent_style = space +indent_size = 2 +insert_final_newline = false + +[*.{yaml,yml}] +indent_style = space +indent_size = 4 +insert_final_newline = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d0ec7f --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +workreportmgr +*.exe +*.test diff --git a/export/latex/export.go b/export/latex/export.go new file mode 100644 index 0000000..1c8b4f7 --- /dev/null +++ b/export/latex/export.go @@ -0,0 +1,49 @@ +package latex + +import ( + "io" + "text/template" + + "github.com/nicksnyder/go-i18n/i18n" + + "git.dekart811.net/icedream/workreportmgr/project" +) + +// TexMarker represents information about a TeX document to be read in by the +// respective compiler. It contains information for example about which TeX +// variant to use. +type TexMarker struct { + Program string +} + +// Exporter provides functionality to export a workreports project to a LaTeX +// file. +type Exporter struct { + Locale string + Inputs []string + Marker TexMarker +} + +// Export generates LaTeX code from the given project and writes it to the given +// writer. +func (e *Exporter) Export(prj *project.Project, w io.Writer) (err error) { + T, err := i18n.Tfunc(e.Locale) + if err != nil { + return + } + + exportTemplate.Funcs(template.FuncMap{ + "T": T, + }) + + data := struct { + Project *project.Project + TexMarker TexMarker + TexInputs []string + }{ + Project: prj, + TexInputs: e.Inputs, + TexMarker: e.Marker, + } + return exportTemplate.Execute(w, data) +} diff --git a/export/latex/init.go b/export/latex/init.go new file mode 100644 index 0000000..46d01b7 --- /dev/null +++ b/export/latex/init.go @@ -0,0 +1,6 @@ +package latex + +func init() { + initLocalization() + initTemplate() +} diff --git a/export/latex/localization.go b/export/latex/localization.go new file mode 100644 index 0000000..e873957 --- /dev/null +++ b/export/latex/localization.go @@ -0,0 +1,29 @@ +package latex + +import ( + "log" + + "github.com/nicksnyder/go-i18n/i18n" +) + +//go:generate go-bindata -pkg latex -o localization_assets.go localization/ + +func initLocalization() { + log.Println("Initializing localization for LaTeX export...") + + files, err := AssetDir("localization") + if err != nil { + log.Fatal("Failed to browse embedded asset directory.", err) + panic(err) + } + + for _, file := range files { + if localizationBytes, err := Asset("localization/" + file); err != nil { + log.Fatal("Failed to read localization file.", err) + panic(err) + } else if err := i18n.ParseTranslationFileBytes(file, localizationBytes); err != nil { + log.Fatal("Failed to parse localization file.", err) + panic(err) + } + } +} diff --git a/export/latex/localization/de-de.all.json b/export/latex/localization/de-de.all.json new file mode 100644 index 0000000..48c9692 --- /dev/null +++ b/export/latex/localization/de-de.all.json @@ -0,0 +1,54 @@ +[ + { + "id": "calendar_week", + "translation": "Kalenderwoche" + }, + { + "id": "calendar_week_short", + "translation": "KW" + }, + { + "id": "department", + "translation": "Abteilung" + }, + { + "id": "instructor", + "translation": "Ausbilder/in" + }, + { + "id": "legal_representative", + "translation": "Gesetzlicher Vertreter" + }, + { + "id": "name", + "translation": "Name" + }, + { + "id": "no_school_periods_this_week", + "translation": "Hat in dieser Woche noch nicht stattgefunden." + }, + { + "id": "operational_activities", + "translation": "Betriebliche Tätigkeiten" + }, + { + "id": "operational_instruction", + "translation": "Betrieblicher Unterricht" + }, + { + "id": "professional_school", + "translation": "Berufsschule" + }, + { + "id": "proof_of_education", + "translation": "Ausbildungsnachweis Nr. {{.Count}}" + }, + { + "id": "trainee", + "translation": "Auszubildende/r" + }, + { + "id": "time_period", + "translation": "Zeitraum" + } +] \ No newline at end of file diff --git a/export/latex/localization/de-de.untranslated.json b/export/latex/localization/de-de.untranslated.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/export/latex/localization/de-de.untranslated.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/export/latex/localization/en-us.all.json b/export/latex/localization/en-us.all.json new file mode 100644 index 0000000..408af71 --- /dev/null +++ b/export/latex/localization/en-us.all.json @@ -0,0 +1,54 @@ +[ + { + "id": "calendar_week", + "translation": "Calendar week" + }, + { + "id": "calendar_week_short", + "translation": "CW" + }, + { + "id": "department", + "translation": "Department" + }, + { + "id": "instructor", + "translation": "Instructor" + }, + { + "id": "legal_representative", + "translation": "Legal representative" + }, + { + "id": "name", + "translation": "Name" + }, + { + "id": "no_school_periods_this_week", + "translation": "No school periods this week." + }, + { + "id": "operational_activities", + "translation": "Operational activities" + }, + { + "id": "operational_instruction", + "translation": "Operational instruction" + }, + { + "id": "professional_school", + "translation": "Professional school" + }, + { + "id": "proof_of_education", + "translation": "Proof of Education No. {{.Count}}" + }, + { + "id": "trainee", + "translation": "Trainee" + }, + { + "id": "time_period", + "translation": "Time period" + } +] \ No newline at end of file diff --git a/export/latex/localization/en-us.untranslated.json b/export/latex/localization/en-us.untranslated.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/export/latex/localization/en-us.untranslated.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/export/latex/localization_assets.go b/export/latex/localization_assets.go new file mode 100644 index 0000000..d1843fd --- /dev/null +++ b/export/latex/localization_assets.go @@ -0,0 +1,306 @@ +// Code generated by go-bindata. +// sources: +// localization/de-de.all.json +// localization/de-de.untranslated.json +// localization/en-us.all.json +// localization/en-us.untranslated.json +// DO NOT EDIT! + +package latex + +import ( + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) + +func bindataRead(data []byte, name string) ([]byte, error) { + gz, err := gzip.NewReader(bytes.NewBuffer(data)) + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + + var buf bytes.Buffer + _, err = io.Copy(&buf, gz) + clErr := gz.Close() + + if err != nil { + return nil, fmt.Errorf("Read %q: %v", name, err) + } + if clErr != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _localizationDeDeAllJson = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x84\xd2\xc1\x6a\xdb\x40\x10\x06\xe0\x73\xfc\x14\x83\xce\xc1\xb9\xf7\x96\xf6\xd0\x42\x21\xa7\xb6\x81\x96\x22\xd6\xab\x5f\xd2\x90\xf5\xac\x98\x99\x8d\x21\xc6\x6f\xd3\x37\xe9\x8b\x15\xc9\x31\xb4\x45\xeb\x9c\x77\xff\x6f\xa5\x99\xff\xc7\x86\xe8\xb8\x21\x22\x6a\xb8\x6b\xde\x51\x13\x43\x82\x74\x41\xdb\x03\xf0\xd4\xdc\x9e\x8f\x5c\x83\x58\x0a\xce\x59\xe6\x3b\x9f\x97\x3b\xd0\x43\x8e\x23\x9a\x0d\xd1\xe9\xf6\x2a\xd3\xda\x98\xd5\x6b\xd8\xe3\xba\xd0\x61\x0a\xea\x7b\x48\x2d\x78\xbf\x73\x70\x2a\x32\xac\xe7\x59\xcc\xb5\x44\xcf\x5a\xcb\x17\xdb\x71\xea\xa0\x77\x2c\xeb\x44\xc2\x10\x52\xab\x98\x14\x06\xf1\xe0\xfc\x8c\x0a\xf6\x11\x06\x7f\x49\x1c\x47\x28\x7d\x83\xba\xc2\xa1\xeb\xac\x84\x7d\x8d\x79\x98\x8f\xd6\x43\xb9\xb5\x38\xe6\x9c\xda\x09\xca\xb9\xb3\xd6\x47\xb6\x6b\x5b\xfa\x14\x9c\x58\xa8\x63\x18\x94\x1e\xe7\x5d\x91\xe4\x38\x92\x70\x1c\x9d\xcc\x83\xfb\x80\xbe\x48\x07\xd9\xae\x3f\x9a\x27\xe8\xe2\x85\xd4\x86\xe8\xfc\xcc\xce\xb0\xca\x7b\xef\xe1\xca\xd8\x2d\x33\xa0\x2f\xbf\x7f\x39\x0f\x4f\x60\x47\x65\xb8\x7f\xdb\x97\x5d\xcd\xd2\xdb\xb8\xd2\x57\x71\xa8\xce\xbf\xb1\x6e\x4f\x9a\x7b\x98\x9d\xf1\xf3\xd8\xaa\xae\x96\xde\x2c\x8e\x25\x55\x06\x3f\x69\xce\x7d\x9b\xfb\x16\x5d\x89\xe1\xca\x27\xbe\xf6\xa9\xc8\x60\x12\xe2\x78\x00\x1b\x3d\xe8\x96\x8e\xc7\xed\x87\x5c\xc4\x4f\xa7\xf5\x07\x5c\x03\x0b\x6a\x8d\xb8\x2f\xf6\x52\x96\x9e\x4a\x87\xbb\x7f\x1b\x75\x73\x11\x78\x8f\xd7\x5a\x2c\xca\xcd\xff\xc8\x77\xb0\x6b\x28\xfb\x25\xbd\xf9\xf9\x27\x00\x00\xff\xff\x2b\xa3\xd3\x89\xf5\x03\x00\x00") + +func localizationDeDeAllJsonBytes() ([]byte, error) { + return bindataRead( + _localizationDeDeAllJson, + "localization/de-de.all.json", + ) +} + +func localizationDeDeAllJson() (*asset, error) { + bytes, err := localizationDeDeAllJsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "localization/de-de.all.json", size: 1013, mode: os.FileMode(436), modTime: time.Unix(1486647823, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _localizationDeDeUntranslatedJson = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8a\x8e\x05\x04\x00\x00\xff\xff\x29\xbb\x4c\x0d\x02\x00\x00\x00") + +func localizationDeDeUntranslatedJsonBytes() ([]byte, error) { + return bindataRead( + _localizationDeDeUntranslatedJson, + "localization/de-de.untranslated.json", + ) +} + +func localizationDeDeUntranslatedJson() (*asset, error) { + bytes, err := localizationDeDeUntranslatedJsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "localization/de-de.untranslated.json", size: 2, mode: os.FileMode(436), modTime: time.Unix(1479981513, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _localizationEnUsAllJson = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x84\x92\xc1\x4a\xc3\x40\x10\x86\xcf\xcd\x53\x0c\x39\x97\x3e\x80\xd7\xea\x41\x90\xda\x83\xe0\x41\x64\x19\x92\x89\x1d\x4c\x76\xc2\xec\xb4\x1e\x4a\xdf\x5d\x92\x6c\x69\xd0\xdd\xf4\x3c\xdf\xf7\xb5\xc9\x9f\x8f\x02\xe0\x5c\x00\x00\x94\x5c\x97\x0f\x50\x56\xd8\x92\xaf\x51\xdd\x0f\xd1\x77\xb9\x9e\x4e\xa6\xe8\x43\x8b\xc6\xe2\x07\x66\x1b\x19\x18\x99\x02\xe0\xb2\x5e\xcc\xb8\x70\x10\xb5\x5c\xec\x3d\x5d\xa8\xa9\x47\xb5\x8e\x7c\x4e\x7c\xbc\x01\xc9\x00\xfb\x60\x7a\xac\x4c\x34\x13\x78\xbe\x01\xc9\x40\x4b\x5f\xd8\x3a\xa5\x5e\x29\x90\x37\x34\x3e\x51\x26\xf5\x32\xa0\xf0\x07\x4d\x46\x3d\x76\xb9\xc8\x6e\x38\xa5\x25\x71\xa1\x3a\x88\xb4\xae\x27\x65\xa9\x83\xb3\x03\x87\xa5\x89\x76\x02\x93\x01\xd1\x80\xc1\x18\x07\xdb\xa4\x7f\x43\x7a\xd2\x51\xc7\xd6\x61\x65\x7c\x62\x63\x0a\x99\xfc\xeb\x0d\x86\x19\x7c\x37\x7c\xdd\x64\xc8\xdc\x2f\xcf\xe9\x64\xba\x57\x69\x28\x84\xa9\x3d\x3d\x6f\x26\xbb\x9f\x91\xf1\xcd\x64\x93\xd2\x38\x69\x1c\xd5\xc7\x0a\x17\xfe\xe8\x7e\x00\x41\x1a\x78\xba\x82\xb0\x93\x0d\x9c\xcf\x9b\xad\x1c\xbd\x5d\x2e\xe9\xbe\x29\xb2\xa7\xdc\x27\xf0\x16\xaf\x73\x75\x75\x35\xb9\xa3\xb8\xff\x68\xaf\xfe\xc9\xdc\x51\x9c\x7b\x0c\x14\x9f\xbf\x01\x00\x00\xff\xff\x24\x12\x73\xee\xde\x03\x00\x00") + +func localizationEnUsAllJsonBytes() ([]byte, error) { + return bindataRead( + _localizationEnUsAllJson, + "localization/en-us.all.json", + ) +} + +func localizationEnUsAllJson() (*asset, error) { + bytes, err := localizationEnUsAllJsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "localization/en-us.all.json", size: 990, mode: os.FileMode(436), modTime: time.Unix(1486647790, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _localizationEnUsUntranslatedJson = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x8a\x8e\x05\x04\x00\x00\xff\xff\x29\xbb\x4c\x0d\x02\x00\x00\x00") + +func localizationEnUsUntranslatedJsonBytes() ([]byte, error) { + return bindataRead( + _localizationEnUsUntranslatedJson, + "localization/en-us.untranslated.json", + ) +} + +func localizationEnUsUntranslatedJson() (*asset, error) { + bytes, err := localizationEnUsUntranslatedJsonBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "localization/en-us.untranslated.json", size: 2, mode: os.FileMode(436), modTime: time.Unix(1479981513, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "localization/de-de.all.json": localizationDeDeAllJson, + "localization/de-de.untranslated.json": localizationDeDeUntranslatedJson, + "localization/en-us.all.json": localizationEnUsAllJson, + "localization/en-us.untranslated.json": localizationEnUsUntranslatedJson, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "localization": &bintree{nil, map[string]*bintree{ + "de-de.all.json": &bintree{localizationDeDeAllJson, map[string]*bintree{}}, + "de-de.untranslated.json": &bintree{localizationDeDeUntranslatedJson, map[string]*bintree{}}, + "en-us.all.json": &bintree{localizationEnUsAllJson, map[string]*bintree{}}, + "en-us.untranslated.json": &bintree{localizationEnUsUntranslatedJson, map[string]*bintree{}}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/export/latex/stringutil/converter.go b/export/latex/stringutil/converter.go new file mode 100644 index 0000000..f6085fd --- /dev/null +++ b/export/latex/stringutil/converter.go @@ -0,0 +1,5 @@ +package stringutil + +type converter interface { + Process(text string) string +} diff --git a/export/latex/stringutil/converter_simpleregex.go b/export/latex/stringutil/converter_simpleregex.go new file mode 100644 index 0000000..ff9d857 --- /dev/null +++ b/export/latex/stringutil/converter_simpleregex.go @@ -0,0 +1,19 @@ +package stringutil + +import "regexp" + +type simpleRegexConverter struct { + regex *regexp.Regexp + replacement string +} + +func newSimpleRegexConverter(regexStr string, replacementStr string) *simpleRegexConverter { + return &simpleRegexConverter{ + regex: regexp.MustCompile(regexStr), + replacement: replacementStr, + } +} + +func (conv *simpleRegexConverter) Process(text string) string { + return conv.regex.ReplaceAllString(text, conv.replacement) +} diff --git a/export/latex/stringutil/converter_string.go b/export/latex/stringutil/converter_string.go new file mode 100644 index 0000000..4909784 --- /dev/null +++ b/export/latex/stringutil/converter_string.go @@ -0,0 +1,19 @@ +package stringutil + +import "strings" + +type stringConverter struct { + old string + replacement string +} + +func newStringConverter(old string, replacementStr string) *stringConverter { + return &stringConverter{ + old: old, + replacement: replacementStr, + } +} + +func (conv *stringConverter) Process(text string) string { + return strings.Replace(text, conv.old, conv.replacement, -1) +} diff --git a/export/latex/stringutil/escape.go b/export/latex/stringutil/escape.go new file mode 100644 index 0000000..c7e382d --- /dev/null +++ b/export/latex/stringutil/escape.go @@ -0,0 +1,28 @@ +package stringutil + +var replacements = []converter{ + newStringConverter("{", "\\{"), + newStringConverter("}", "\\}"), + newStringConverter("\\", "\\textbackslash{}"), + + newStringConverter("&", "\\&"), + newStringConverter("%", "\\%"), + newStringConverter("$", "\\$"), + newStringConverter("#", "\\#"), + newStringConverter("_", "\\_"), + newStringConverter("~", "\\textasciitilde{}"), + newStringConverter("^", "\\textasciicircum{}"), + newStringConverter("ß", "\\ss{}"), + + newSimpleRegexConverter(`"([^"]+)"`, `\enquote{$1}`), +} + +// TexEscape modifies a string so it can be safely places in a LaTeX file +// without causing any errors due to special characters. +func TexEscape(s string) string { + for _, replacer := range replacements { + s = replacer.Process(s) + } + + return s +} diff --git a/export/latex/template.go b/export/latex/template.go new file mode 100644 index 0000000..1bb522a --- /dev/null +++ b/export/latex/template.go @@ -0,0 +1,152 @@ +package latex + +import ( + "log" + "os" + "text/template" + + "git.dekart811.net/icedream/workreportmgr/export/latex/stringutil" + "git.dekart811.net/icedream/workreportmgr/project" + "github.com/jinzhu/now" + "github.com/nicksnyder/go-i18n/i18n" +) + +const dateFormat = "02.01.2006" // TODO - localize! + +var exportTemplate = template.Must(template. + New("latex_export"). + Funcs(template.FuncMap{ + "T": i18n.IdentityTfunc, + "escape": stringutil.TexEscape, + "add": func(a, b int) int { + return a + b + }, + "beginofweek": func(date project.Date) project.Date { + return project.Date{now.New(date.Time).BeginningOfWeek()} + }, + "endofweek": func(date project.Date) project.Date { + return project.Date{now.New(date.Time).EndOfWeek()} + }, + }). + Delims("<", ">"). + Parse(`% !TeX +<- with .TexMarker.Program> program = <.> + +\documentclass[11pt,a4paper,oneside]{article} + +\usepackage{fancyhdr} +\usepackage{tabularx} +\usepackage[left=2cm,right=2cm,top=2cm,bottom=6cm,includeheadfoot]{geometry} +\usepackage{csquotes} +\usepackage{fontspec} +\usepackage{ifluatex} +\ifluatex +\else + \usepackage[utf8]{inputenc} +\fi + +\pagestyle{fancy} +\fancyhf{} +\setlength{\headheight}{2cm} + +\newcommand{\Name}{<.Project.Name>} +\newcommand{\Department}{<.Project.Department>} + +\newcommand{\wrSigningField}[0]{ + \begin{tabularx}{\textwidth}{| X | X | X |} + \hline + & + & + \\[2cm] + \hline + \end{tabularx} +} + +\newenvironment{weeklyreport}{ +}{ + \fancyfoot[C]{\wrSigningField} +} + +\newcommand{\preweeklyreporthead}[3]{} + +\newcommand{\weeklyreporthead}[3]{ + \preweeklyreporthead{#1}{#2}{#3} + \fancyhead[R]{ + \begin{tabularx}{8cm}{rl} + \textbf{}: & \Name \\ + \textbf{}: & \Department \\ + \textbf{}: & #2 - #3 \\ + \end{tabularx} + } + \fancyfoot{} + \newpage + \setcounter{section}{#1} + \setcounter{subsection}{0} + \section*{} + \addcontentsline{toc}{section}{} +} + +\newcommand{\weeklyreportsection}[1]{ + \subsection*{#1} +} + + + +\input{<.>} + + + +\begin{document} +\tableofcontents + + +\begin{weeklyreport} + \weeklyreporthead{}{}{} + + \weeklyreportsection{} + + \begin{itemize} + + \item <. | escape> + + \end{itemize} + + + \weeklyreportsection{} + <$week.WorkActivityDetails | escape> + + \weeklyreportsection{} + + + + \begin{itemize} + + \item{ + <- .Subject | escape -> + <- with .Topics ->: + + <- if ne $index 0 ->, <- $topic | escape -> + <- end -> + <- end -> + } + + \end{itemize} + + +\end{weeklyreport} + + +\end{document} +`)) + +func initTemplate() { + log.Println("Initializing template for LaTeX export...") + + var err error + defer func() { + if err != nil { + log.Fatal("Failed at initializing template.", err) + os.Exit(0xFF) + } + }() +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..fb3da04 --- /dev/null +++ b/main.go @@ -0,0 +1,105 @@ +package main + +import ( + "log" + "os" + + "git.dekart811.net/icedream/workreportmgr/export/latex" + "git.dekart811.net/icedream/workreportmgr/project" + + "github.com/alecthomas/kingpin" +) + +var ( + cli = kingpin.New("workreport-manager", "Manage all of your workreports through an easy to read and manage format.") + + flagProjectFile = cli.Flag("file", "Defines the project filename from which to parse all workreports.").Short('f').Default("workreports.yml").File() + flagVerbose = cli.Flag("verbose", "Print extra information about what's happening.").Short('v').Bool() + flagLocale = cli.Flag("locale", "The locale to use for exports.").Default("en-US").String() + + cmdInit = cli.Command("init", "Creates a new workreports project file.") + + cmdAdd = cli.Command("add", "Add new information to the workreport.") + + cmdAddActivity = cmdAdd.Command("activity", "Adds a new work activity to the workreport for the current date.") + + cmdAddPeriod = cmdAdd.Command("period", "Adds a new school period to the workreport for the current date.") + + cmdAddActivityDetails = cmdAdd.Command("details", "Sets the detail information for a work activity for the current date.") + cmdAddActivityDetailsFlagInput = cmdAddActivityDetails.Flag("input", "The file from which to read the information. Defaults to stdin.").Short('i').Default(os.Stdin.Name()).ExistingFile() + + cmdExportToLatex = cli.Command("latex", "Exports the current workreport project to Latex.") + cmdExportToLatexFlagOutput = cmdExportToLatex.Flag("output", "The file to write the generated code to. Defaults to stdout.").Short('o').Default(os.Stdout.Name()).String() + cmdExportToLatexFlagInput = cmdExportToLatex.Flag("input", "Use this to write additional \\input statements in the resulting LaTeX file.").Short('i').Strings() + cmdExportToLatexFlagProgram = cmdExportToLatex.Flag("program", "The LaTeX compiler program to use, for example \"lualatex\". Leave empty if you don't know what to put here.").Short('p').String() + + version = "master" + + currentProject *project.Project +) + +func main() { + cli.Version(version) + if *flagVerbose { + log.Printf("%s v%s", cli.Name, version) + } + + command := kingpin.MustParse(cli.Parse(os.Args[1:])) + + switch command { + case cmdInit.FullCommand(): + log.Println("Not yet implemented") // TODO + case cmdAddActivity.FullCommand(): + log.Println("Not yet implemented") // TODO + case cmdAddActivityDetails.FullCommand(): + log.Println("Not yet implemented") // TODO + case cmdAddPeriod.FullCommand(): + log.Println("Not yet implemented") // TODO + case cmdExportToLatex.FullCommand(): + parseProject() + if *flagVerbose { + log.Println("Going to export to Latex file...") + } + w, err := os.Create(*cmdExportToLatexFlagOutput) + if err != nil { + log.Print("Failed to create output file.") + os.Exit(1) + } + log.Println("Created output file.") + defer w.Close() + exporter := new(latex.Exporter) + if flagLocale != nil { + exporter.Locale = *flagLocale + } + if cmdExportToLatexFlagInput != nil { + exporter.Inputs = *cmdExportToLatexFlagInput + } + if cmdExportToLatexFlagProgram != nil { + exporter.Marker.Program = *cmdExportToLatexFlagProgram + } + log.Println("Now exporting...") + if err := exporter.Export(currentProject, w); err != nil { + log.Print("Failed to generate LaTeX output:", err) + os.Exit(1) + } + log.Println("Done.") + default: + log.Println("Unknown command.") + } +} + +func parseProject() { + var err error + defer func() { + if err != nil { + log.Print("Failed to parse project file:", err) + os.Exit(1) + } + }() + + currentProject, err = project.DecodeFromFile(*flagProjectFile) + if *flagVerbose { + log.Println("Parsed project file successfully.") + log.Printf("Resulting project: %+v", currentProject) + } +} diff --git a/project/date.go b/project/date.go new file mode 100644 index 0000000..eb41452 --- /dev/null +++ b/project/date.go @@ -0,0 +1,31 @@ +package project + +import ( + "fmt" + "strings" + "time" +) + +const dateFormat = "2006-01-02" + +// Date is a wrapper around time.Time with a different UnmarshalJSON +// implementation that parses just date information. +type Date struct { + time.Time +} + +// UnmarshalJSON takes the given buffer and translates it into a proper +// time.Time object. +func (t *Date) UnmarshalJSON(buf []byte) (err error) { + tt, err := time.Parse(dateFormat, strings.Trim(string(buf), `"`)) + if err != nil { + return + } + t.Time = tt + return nil +} + +func (t Date) String() string { + // TODO - Localize date! + return fmt.Sprintf("%02d.%02d.%04d", t.Time.Day(), t.Time.Month(), t.Time.Year()) +} diff --git a/project/date_test.go b/project/date_test.go new file mode 100644 index 0000000..11b6b0e --- /dev/null +++ b/project/date_test.go @@ -0,0 +1,31 @@ +package project_test + +import ( + "testing" + "time" + + "git.dekart811.net/icedream/workreportmgr/project" + + "github.com/stretchr/testify/require" +) + +func TestDate_String(t *testing.T) { + date := project.Date{time.Date(2016, 8, 22, 11, 22, 33, 97, time.Local)} + require.Equal(t, "22.08.2016", date.String()) +} + +func TestDate_UnmarshalJSON_Quotes(t *testing.T) { + date := project.Date{} + require.Nil(t, date.UnmarshalJSON([]byte(`"2016-08-22"`))) + require.Equal(t, 2016, date.Time.Year()) + require.Equal(t, time.Month(8), date.Time.Month()) + require.Equal(t, 22, date.Time.Day()) +} + +func TestDate_UnmarshalJSON_NoQuotes(t *testing.T) { + date := project.Date{} + require.Nil(t, date.UnmarshalJSON([]byte(`2016-08-22`))) + require.Equal(t, 2016, date.Time.Year()) + require.Equal(t, time.Month(8), date.Time.Month()) + require.Equal(t, 22, date.Time.Day()) +} diff --git a/project/date_white_test.go b/project/date_white_test.go new file mode 100644 index 0000000..c4bc5fe --- /dev/null +++ b/project/date_white_test.go @@ -0,0 +1,16 @@ +package project + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestDate_dateFormat(t *testing.T) { + date, err := time.Parse(dateFormat, "2013-04-05") + require.Nil(t, err) + require.Equal(t, 2013, date.Year()) + require.Equal(t, time.Month(4), date.Month()) + require.Equal(t, 5, date.Day()) +} diff --git a/project/file.go b/project/file.go new file mode 100644 index 0000000..465f303 --- /dev/null +++ b/project/file.go @@ -0,0 +1,40 @@ +package project + +import ( + "io/ioutil" + "os" + + "github.com/ghodss/yaml" +) + +// Marshal takes the data from this Project instance and turns it into a YAML +// text that is easy to read and can easily be decoded with Unmarshal or Decode. +func (prj Project) Marshal() ([]byte, error) { + return yaml.Marshal(prj) +} + +// Unmarshal decodes project information from a given YAML text blob into this +// Project instance. +func (prj *Project) Unmarshal(yamlData []byte) error { + return yaml.Unmarshal(yamlData, prj) +} + +// Decode decodes project information from a given YAML text blob into a new +// instance of Project. +func Decode(yamlData []byte) (prj *Project, err error) { + prj = new(Project) + if err = prj.Unmarshal(yamlData); err != nil { + prj = nil + } + return +} + +// DecodeFromFile decodes project information from a given file into a new +// instance of Project. +func DecodeFromFile(file *os.File) (prj *Project, err error) { + contents, err := ioutil.ReadAll(file) + if err != nil { + return + } + return Decode(contents) +} diff --git a/project/format.go b/project/format.go new file mode 100644 index 0000000..e5e1720 --- /dev/null +++ b/project/format.go @@ -0,0 +1,10 @@ +package project + +// Project represents the root structure of a project file. +type Project struct { + Name string `json:"Name"` + Department string `json:"Department"` + Begin Date `json:"Begin"` + End Date `json:"End"` + Weeks []Week `json:"Weeks"` +} diff --git a/project/period.go b/project/period.go new file mode 100644 index 0000000..4b7310e --- /dev/null +++ b/project/period.go @@ -0,0 +1,7 @@ +package project + +// Period represents a school period in the weekly report. +type Period struct { + Subject string `json:"Subject"` + Topics []string `json:"Topics"` +} diff --git a/project/week.go b/project/week.go new file mode 100644 index 0000000..9b352dd --- /dev/null +++ b/project/week.go @@ -0,0 +1,9 @@ +package project + +// Week represents a week in the report. +type Week struct { + Date Date + WorkActivities []string `json:"Operational activities"` + WorkActivityDetails string `json:"Operational instructions"` + Periods []Period `json:"Professional school"` +}