feat(canary): docx template (binary blob + build helper)
Phase 4 Task 4.1+4.2 — pure-Go OOXML template builder under
backend/cmd/builddocxtemplate/. Run:
go run ./cmd/builddocxtemplate \
-out ./internal/token/generators/docx/template/template.docx
produces a minimal valid .docx (5 zip entries, mixed STORE/DEFLATE
compression) whose word/footer2.xml carries an INCLUDEPICTURE field
referencing the literal placeholder string HONEY_TRACK_URL with the
\\d switch (forces fetch-on-open, no local cache).
The committed template.docx is the embedded source for the Phase 4
docx generator (next commit). Mixed compression methods in the template
make Task 4.3's TestGenerate_PreservesCompressionMethods a meaningful
regression guard — a hardcode-DEFLATE generator would now mismatch on
[Content_Types].xml + word/_rels/document.xml.rels (both STORE).
cmd/builddocxtemplate is excluded from the production binary (separate
cmd/ subdir from cmd/canary).
This commit is contained in:
parent
fe7d641af8
commit
f36cce9a01
|
|
@ -0,0 +1,127 @@
|
|||
// ©AngelaMos | 2026
|
||||
// main.go
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
contentTypesXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
|
||||
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
|
||||
<Default Extension="xml" ContentType="application/xml"/>
|
||||
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
|
||||
<Override PartName="/word/footer2.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml"/>
|
||||
</Types>
|
||||
`
|
||||
|
||||
packageRelsXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
|
||||
</Relationships>
|
||||
`
|
||||
|
||||
documentXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
|
||||
<w:body>
|
||||
<w:p><w:r><w:t xml:space="preserve">Internal document - confidential. Do not redistribute.</w:t></w:r></w:p>
|
||||
<w:sectPr>
|
||||
<w:footerReference w:type="default" r:id="rIdFooter"/>
|
||||
<w:pgSz w:w="12240" w:h="15840"/>
|
||||
<w:pgMar w:top="1440" w:right="1440" w:bottom="1440" w:left="1440" w:header="720" w:footer="720" w:gutter="0"/>
|
||||
</w:sectPr>
|
||||
</w:body>
|
||||
</w:document>
|
||||
`
|
||||
|
||||
documentRelsXML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
|
||||
<Relationship Id="rIdFooter" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" Target="footer2.xml"/>
|
||||
</Relationships>
|
||||
`
|
||||
|
||||
footer2XML = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<w:ftr xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
|
||||
<w:p>
|
||||
<w:r><w:fldChar w:fldCharType="begin"/></w:r>
|
||||
<w:r><w:instrText xml:space="preserve"> INCLUDEPICTURE "HONEY_TRACK_URL" \d \* MERGEFORMAT </w:instrText></w:r>
|
||||
<w:r><w:fldChar w:fldCharType="end"/></w:r>
|
||||
</w:p>
|
||||
</w:ftr>
|
||||
`
|
||||
|
||||
pathContentTypes = "[Content_Types].xml"
|
||||
pathPackageRels = "_rels/.rels"
|
||||
pathDocument = "word/document.xml"
|
||||
pathDocumentRels = "word/_rels/document.xml.rels"
|
||||
pathFooter2 = "word/footer2.xml"
|
||||
|
||||
dirPerm os.FileMode = 0o755
|
||||
filePerm os.FileMode = 0o644
|
||||
)
|
||||
|
||||
type entry struct {
|
||||
name string
|
||||
body string
|
||||
method uint16
|
||||
}
|
||||
|
||||
func entries() []entry {
|
||||
return []entry{
|
||||
{name: pathContentTypes, body: contentTypesXML, method: zip.Store},
|
||||
{name: pathPackageRels, body: packageRelsXML, method: zip.Deflate},
|
||||
{name: pathDocument, body: documentXML, method: zip.Deflate},
|
||||
{name: pathDocumentRels, body: documentRelsXML, method: zip.Store},
|
||||
{name: pathFooter2, body: footer2XML, method: zip.Deflate},
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
out := flag.String("out", "", "output path for template.docx")
|
||||
flag.Parse()
|
||||
|
||||
if *out == "" {
|
||||
log.Fatal("usage: builddocxtemplate -out <path>")
|
||||
}
|
||||
|
||||
if err := buildTemplate(*out); err != nil {
|
||||
log.Fatalf("build docx template: %v", err)
|
||||
}
|
||||
|
||||
fmt.Printf("wrote %s\n", *out)
|
||||
}
|
||||
|
||||
func buildTemplate(out string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(out), dirPerm); err != nil {
|
||||
return fmt.Errorf("mkdir parent: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(out, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, filePerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open output: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w := zip.NewWriter(f)
|
||||
for _, e := range entries() {
|
||||
hdr := &zip.FileHeader{Name: e.name, Method: e.method}
|
||||
fw, cErr := w.CreateHeader(hdr)
|
||||
if cErr != nil {
|
||||
return fmt.Errorf("create %s: %w", e.name, cErr)
|
||||
}
|
||||
if _, wErr := fw.Write([]byte(e.body)); wErr != nil {
|
||||
return fmt.Errorf("write %s: %w", e.name, wErr)
|
||||
}
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return fmt.Errorf("close zip writer: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Binary file not shown.
Loading…
Reference in New Issue