├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── base.go ├── docx ├── docx.go ├── docx_test.go ├── fixtures │ └── test.docx └── utils.go └── mocks └── Document.go /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Welcome! 2 | 3 | We're so glad you're thinking about contributing to an open source project! If you're unsure about anything, just ask -- or submit the issue or pull request anyway. The worst that can happen is you'll be politely asked to change something. We love all friendly contributions. 4 | 5 | We encourage you to read this project's CONTRIBUTING policy (you are here), its [LICENSE](LICENSE.md), and its [README](README.md). 6 | 7 | ## Public domain 8 | 9 | This project is in the public domain within the United States, and 10 | copyright and related rights in the work worldwide are waived through 11 | the [CC0 1.0 Universal public domain dedication](https://creativecommons.org/publicdomain/zero/1.0/). 12 | 13 | All contributions to this project will be released under the CC0 14 | dedication. By submitting a pull request, you are agreeing to comply 15 | with this waiver of copyright interest. 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | As a work of the United States Government, this project is in the 2 | public domain within the United States. 3 | 4 | Additionally, we waive copyright and related rights in the work 5 | worldwide through the CC0 1.0 Universal public domain dedication. 6 | 7 | ## CC0 1.0 Universal Summary 8 | 9 | This is a human-readable summary of the [Legal Code (read the full text)](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 10 | 11 | ### No Copyright 12 | 13 | The person who associated a work with this deed has dedicated the work to 14 | the public domain by waiving all of his or her rights to the work worldwide 15 | under copyright law, including all related and neighboring rights, to the 16 | extent allowed by law. 17 | 18 | You can copy, modify, distribute and perform the work, even for commercial 19 | purposes, all without asking permission. 20 | 21 | ### Other Information 22 | 23 | In no way are the patent or trademark rights of any person affected by CC0, 24 | nor are the rights that other persons may have in the work or in how the 25 | work is used, such as publicity or privacy rights. 26 | 27 | Unless expressly stated otherwise, the person who associated a work with 28 | this deed makes no warranties about the work, and disclaims liability for 29 | all uses of the work, to the fullest extent permitted by applicable law. 30 | When using or citing the work, you should not imply endorsement by the 31 | author or the affirmer. 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Simple Google Go (golang) library for building templates for generic content 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/opencontrol/doc-template)](https://goreportcard.com/report/github.com/opencontrol/doc-template) 3 | 4 | ```go 5 | func main() { 6 | funcMap := template.FuncMap{"title": strings.Title} 7 | docTemp, _ := GetTemplate("docx/fixtures/test.docx") 8 | docTemp.AddFunctions(funcMap) 9 | docTemp.Parse() 10 | docTemp.Execute("test.docx", nil) 11 | } 12 | ``` 13 | -------------------------------------------------------------------------------- /base.go: -------------------------------------------------------------------------------- 1 | package docTemp 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "log" 7 | "path/filepath" 8 | "text/template" 9 | 10 | "github.com/opencontrol/doc-template/docx" 11 | ) 12 | 13 | // Document interface is a combintation of methods use for generic data files 14 | type Document interface { 15 | ReadFile(string) error 16 | UpdateContent(string) 17 | GetContent() string 18 | WriteToFile(string, string) error 19 | Close() error 20 | } 21 | 22 | // DocTemplate struct combines data and methods from both the Document interface 23 | // and golang's templating library 24 | type DocTemplate struct { 25 | Template *template.Template 26 | Document Document 27 | } 28 | 29 | // GetTemplate uses the file extension to determine the correct document struct to use 30 | func GetTemplate(filePath string) (*DocTemplate, error) { 31 | var document Document 32 | switch filepath.Ext(filePath) { 33 | case ".docx": 34 | document = new(docx.Docx) 35 | default: 36 | return nil, errors.New("Unsupported Document Type") 37 | } 38 | err := document.ReadFile(filePath) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return &DocTemplate{Document: document, Template: template.New("docTemp")}, nil 43 | } 44 | 45 | // Execute func runs the template and sends the output to the export path 46 | func (docTemplate *DocTemplate) Execute(exportPath string, data interface{}) error { 47 | buf := new(bytes.Buffer) 48 | err := docTemplate.Template.Execute(buf, data) 49 | if err != nil { 50 | log.Println(err) 51 | return err 52 | } 53 | err = docTemplate.Document.WriteToFile(exportPath, buf.String()) 54 | return err 55 | } 56 | 57 | // AddFunctions adds functions to the template 58 | func (docTemplate *DocTemplate) AddFunctions(funcMap template.FuncMap) { 59 | docTemplate.Template = docTemplate.Template.Funcs(funcMap) 60 | } 61 | 62 | // Parse parses the template 63 | func (docTemplate *DocTemplate) Parse() { 64 | temp, err := docTemplate.Template.Parse(docTemplate.Document.GetContent()) 65 | if err != nil { 66 | log.Println(err) 67 | } else { 68 | docTemplate.Template = temp 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /docx/docx.go: -------------------------------------------------------------------------------- 1 | package docx 2 | 3 | import ( 4 | "archive/zip" 5 | "errors" 6 | "io" 7 | "log" 8 | "os" 9 | ) 10 | 11 | // Docx struct that contains data from a docx 12 | type Docx struct { 13 | zipReader *zip.ReadCloser 14 | content string 15 | } 16 | 17 | // ReadFile func reads a docx file 18 | func (d *Docx) ReadFile(path string) error { 19 | reader, err := zip.OpenReader(path) 20 | if err != nil { 21 | return errors.New("Cannot Open File") 22 | } 23 | content, err := readText(reader.File) 24 | if err != nil { 25 | return errors.New("Cannot Read File") 26 | } 27 | d.zipReader = reader 28 | if content == "" { 29 | return errors.New("File has no content") 30 | } 31 | d.content = cleanText(content) 32 | log.Printf("Read File `%s`", path) 33 | return nil 34 | } 35 | 36 | // UpdateContent updates the content string 37 | func (d *Docx) UpdateContent(newContent string) { 38 | d.content = newContent 39 | } 40 | 41 | // GetContent returns the string content 42 | func (d *Docx) GetContent() string { 43 | return d.content 44 | } 45 | 46 | // WriteToFile writes the changes to a new file 47 | func (d *Docx) WriteToFile(path string, data string) error { 48 | var target *os.File 49 | target, err := os.Create(path) 50 | if err != nil { 51 | return err 52 | } 53 | defer target.Close() 54 | err = d.write(target, data) 55 | if err != nil { 56 | return err 57 | } 58 | log.Printf("Exporting data to %s", path) 59 | return nil 60 | } 61 | 62 | // Close the document 63 | func (d *Docx) Close() error { 64 | return d.zipReader.Close() 65 | } 66 | 67 | func (d *Docx) write(ioWriter io.Writer, data string) error { 68 | var err error 69 | // Reformat string, for some reason the first char is converted to < 70 | w := zip.NewWriter(ioWriter) 71 | for _, file := range d.zipReader.File { 72 | var writer io.Writer 73 | var readCloser io.ReadCloser 74 | writer, err := w.Create(file.Name) 75 | if err != nil { 76 | return err 77 | } 78 | readCloser, err = file.Open() 79 | if err != nil { 80 | return err 81 | } 82 | if file.Name == "word/document.xml" { 83 | writer.Write([]byte(data)) 84 | } else { 85 | writer.Write(streamToByte(readCloser)) 86 | } 87 | } 88 | w.Close() 89 | return err 90 | } 91 | -------------------------------------------------------------------------------- /docx/docx_test.go: -------------------------------------------------------------------------------- 1 | package docx 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | type docxTest struct { 14 | fixture string 15 | content string 16 | err error 17 | } 18 | 19 | var readDocTests = []docxTest{ 20 | // Check that reading a document works 21 | {fixture: "fixtures/test.docx", content: "This is a test document", err: nil}, 22 | } 23 | 24 | func TestReadFile(t *testing.T) { 25 | for _, example := range readDocTests { 26 | actualDoc := new(Docx) 27 | actualErr := actualDoc.ReadFile(example.fixture) 28 | assert.Equal(t, example.err, actualErr) 29 | if actualErr == nil { 30 | assert.Contains(t, actualDoc.content, example.content) 31 | } 32 | } 33 | } 34 | 35 | var writeDocTests = []docxTest{ 36 | // Check that writing a document works 37 | {fixture: "fixtures/test.docx", content: "This is an addition", err: nil}, 38 | } 39 | 40 | func TestWriteToFile(t *testing.T) { 41 | for _, example := range writeDocTests { 42 | exportTempDir, _ := ioutil.TempDir("", "exports") 43 | // Overwrite content 44 | actualDoc := new(Docx) 45 | actualDoc.ReadFile(example.fixture) 46 | currentContent := actualDoc.GetContent() 47 | actualDoc.UpdateContent(strings.Replace(currentContent, "This is a test document", example.content, -1)) 48 | newFilePath := filepath.Join(exportTempDir, "test.docx") 49 | actualDoc.WriteToFile(newFilePath, actualDoc.GetContent()) 50 | // Check content 51 | newActualDoc := new(Docx) 52 | newActualDoc.ReadFile(newFilePath) 53 | assert.Contains(t, newActualDoc.GetContent(), example.content) 54 | os.RemoveAll(exportTempDir) 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /docx/fixtures/test.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/opencontrol/doc-template/dc8b9ba59eec874e0d69b1c944d6d2a1279b8c27/docx/fixtures/test.docx -------------------------------------------------------------------------------- /docx/utils.go: -------------------------------------------------------------------------------- 1 | package docx 2 | 3 | import ( 4 | "archive/zip" 5 | "bytes" 6 | "errors" 7 | "io" 8 | "io/ioutil" 9 | "regexp" 10 | "strings" 11 | ) 12 | 13 | // readText reads text from a word document 14 | func readText(files []*zip.File) (text string, err error) { 15 | var documentFile *zip.File 16 | documentFile, err = retrieveWordDoc(files) 17 | if err != nil { 18 | return text, err 19 | } 20 | var documentReader io.ReadCloser 21 | documentReader, err = documentFile.Open() 22 | if err != nil { 23 | return text, err 24 | } 25 | 26 | text, err = wordDocToString(documentReader) 27 | return 28 | } 29 | 30 | // wordDocToString converts a word document to string 31 | func wordDocToString(reader io.Reader) (string, error) { 32 | b, err := ioutil.ReadAll(reader) 33 | if err != nil { 34 | return "", err 35 | } 36 | return string(b), nil 37 | } 38 | 39 | // retrieveWordDoc fetches a word document. 40 | func retrieveWordDoc(files []*zip.File) (file *zip.File, err error) { 41 | for _, f := range files { 42 | if f.Name == "word/document.xml" { 43 | file = f 44 | } 45 | } 46 | if file == nil { 47 | err = errors.New("document.xml file not found") 48 | } 49 | return 50 | } 51 | 52 | func streamToByte(stream io.Reader) []byte { 53 | buf := new(bytes.Buffer) 54 | buf.ReadFrom(stream) 55 | return buf.Bytes() 56 | } 57 | 58 | // normalize fixes quotation marks in documnet 59 | func normalizeQuotes(in rune) rune { 60 | switch in { 61 | case '“', '”': 62 | return '"' 63 | case '‘', '’': 64 | return '\'' 65 | } 66 | return in 67 | } 68 | 69 | // cleans template tagged text of all brakets 70 | func normalizeAll(text string) string { 71 | brakets := regexp.MustCompile("<.*?>") 72 | quotes := regexp.MustCompile(""") 73 | text = brakets.ReplaceAllString(text, "") 74 | text = quotes.ReplaceAllString(text, "\"") 75 | return strings.Map(normalizeQuotes, text) 76 | } 77 | 78 | func cleanText(text string) string { 79 | braketFinder := regexp.MustCompile("{{.*?}}") 80 | return braketFinder.ReplaceAllStringFunc(text, normalizeAll) 81 | } 82 | -------------------------------------------------------------------------------- /mocks/Document.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "github.com/stretchr/testify/mock" 4 | 5 | // Document struct contains the mock methods for testing 6 | type Document struct { 7 | mock.Mock 8 | } 9 | 10 | // ReadFile provides a mock function with given fields: _a0 11 | func (_m *Document) ReadFile(_a0 string) error { 12 | ret := _m.Called(_a0) 13 | 14 | var r0 error 15 | if rf, ok := ret.Get(0).(func(string) error); ok { 16 | r0 = rf(_a0) 17 | } else { 18 | r0 = ret.Error(0) 19 | } 20 | 21 | return r0 22 | } 23 | 24 | // UpdateContent provides a mock function with given fields: _a0 25 | func (_m *Document) UpdateContent(_a0 string) { 26 | _m.Called(_a0) 27 | } 28 | 29 | // GetContent provides a mock function with given fields: 30 | func (_m *Document) GetContent() string { 31 | ret := _m.Called() 32 | 33 | var r0 string 34 | if rf, ok := ret.Get(0).(func() string); ok { 35 | r0 = rf() 36 | } else { 37 | r0 = ret.Get(0).(string) 38 | } 39 | 40 | return r0 41 | } 42 | 43 | // WriteToFile provides a mock function with given fields: _a0, _a1 44 | func (_m *Document) WriteToFile(_a0 string, _a1 string) error { 45 | ret := _m.Called(_a0, _a1) 46 | 47 | var r0 error 48 | if rf, ok := ret.Get(0).(func(string, string) error); ok { 49 | r0 = rf(_a0, _a1) 50 | } else { 51 | r0 = ret.Error(0) 52 | } 53 | 54 | return r0 55 | } 56 | 57 | // Close provides a mock function with given fields: 58 | func (_m *Document) Close() error { 59 | ret := _m.Called() 60 | 61 | var r0 error 62 | if rf, ok := ret.Get(0).(func() error); ok { 63 | r0 = rf() 64 | } else { 65 | r0 = ret.Error(0) 66 | } 67 | 68 | return r0 69 | } 70 | --------------------------------------------------------------------------------