Merge: t-paliad-364 styled+filled submissions — project-less caption fill (P3b) + merge-safe styled firm-skeleton generator (P3a Option B)
This commit is contained in:
@@ -170,9 +170,27 @@ func (s *SubmissionVarsService) Build(ctx context.Context, in SubmissionVarsCont
|
||||
|
||||
if in.ProjectID == nil {
|
||||
// Project-less draft (t-paliad-243): no project / parties /
|
||||
// deadline state to resolve. The lawyer's overrides will fill
|
||||
// the placeholder map; missing keys render as
|
||||
// [KEIN WERT: …] / [NO VALUE: …] in the preview.
|
||||
// deadline state to resolve — but the draft still carries a
|
||||
// submission_code, and the rule loaded above carries its
|
||||
// ProceedingTypeID. Fill-what-we-can (t-paliad-364 P3b): resolve
|
||||
// the caption wording (heading / designations / versus / subject)
|
||||
// and the project.proceeding.* line from that proceeding, so the
|
||||
// Rubrum renders correct procedural wording instead of a wall of
|
||||
// [KEIN WERT]. Only the genuinely project-specific values (party
|
||||
// names, case number, court) stay blank for the lawyer to fill.
|
||||
//
|
||||
// loadProceedingType tolerates a nil id; resolveCaption (via
|
||||
// captionResolver) and addProjectVars both tolerate a nil project,
|
||||
// so this is safe even when the rule has no proceeding_type_id.
|
||||
pt, err := s.loadProceedingType(ctx, rule.ProceedingTypeID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolvers = append(resolvers,
|
||||
projectResolver{project: nil, pt: pt, lang: lang},
|
||||
captionResolver{project: nil, pt: pt, lang: lang},
|
||||
)
|
||||
out.ProceedingType = pt
|
||||
out.Placeholders = docforge.NewResolverSet(resolvers...).BuildBag()
|
||||
return out, nil
|
||||
}
|
||||
@@ -356,26 +374,35 @@ func addUserVars(bag PlaceholderMap, u *models.User) {
|
||||
|
||||
// addProjectVars populates project.* — title / case_number / court /
|
||||
// patent_number / dates / our_side / proceeding metadata.
|
||||
//
|
||||
// A nil project is tolerated so this resolver can run as a proceeding-only
|
||||
// resolver on the project-less draft path (t-paliad-364 P3b): the
|
||||
// project.proceeding.* keys still fill from pt (which the submission_code's
|
||||
// rule supplies), while the genuinely project-specific keys (title /
|
||||
// case_number / court / …) stay unset and render as honest [KEIN WERT] gaps
|
||||
// for the lawyer.
|
||||
func addProjectVars(bag PlaceholderMap, p *models.Project, pt *models.ProceedingType, lang string) {
|
||||
bag["project.title"] = p.Title
|
||||
bag["project.reference"] = derefString(p.Reference)
|
||||
bag["project.case_number"] = derefString(p.CaseNumber)
|
||||
bag["project.court"] = derefString(p.Court)
|
||||
bag["project.patent_number"] = derefString(p.PatentNumber)
|
||||
// project.patent_number_upc is the UPC-brief convention — kind code
|
||||
// parenthesised ("EP 1 234 567 (B1)") instead of the DE form
|
||||
// ("EP 1 234 567 B1"). Pure-function rewrite; pass-through when no
|
||||
// kind code is present so the lawyer's draft never sees a worse
|
||||
// number than the source value.
|
||||
bag["project.patent_number_upc"] = patentNumberUPC(derefString(p.PatentNumber))
|
||||
bag["project.filing_date"] = formatDatePtr(p.FilingDate, "2006-01-02")
|
||||
bag["project.grant_date"] = formatDatePtr(p.GrantDate, "2006-01-02")
|
||||
bag["project.our_side"] = derefString(p.OurSide)
|
||||
bag["project.our_side_de"] = ourSideDE(derefString(p.OurSide))
|
||||
bag["project.our_side_en"] = ourSideEN(derefString(p.OurSide))
|
||||
bag["project.instance_level"] = derefString(p.InstanceLevel)
|
||||
bag["project.client_number"] = derefString(p.ClientNumber)
|
||||
bag["project.matter_number"] = derefString(p.MatterNumber)
|
||||
if p != nil {
|
||||
bag["project.title"] = p.Title
|
||||
bag["project.reference"] = derefString(p.Reference)
|
||||
bag["project.case_number"] = derefString(p.CaseNumber)
|
||||
bag["project.court"] = derefString(p.Court)
|
||||
bag["project.patent_number"] = derefString(p.PatentNumber)
|
||||
// project.patent_number_upc is the UPC-brief convention — kind code
|
||||
// parenthesised ("EP 1 234 567 (B1)") instead of the DE form
|
||||
// ("EP 1 234 567 B1"). Pure-function rewrite; pass-through when no
|
||||
// kind code is present so the lawyer's draft never sees a worse
|
||||
// number than the source value.
|
||||
bag["project.patent_number_upc"] = patentNumberUPC(derefString(p.PatentNumber))
|
||||
bag["project.filing_date"] = formatDatePtr(p.FilingDate, "2006-01-02")
|
||||
bag["project.grant_date"] = formatDatePtr(p.GrantDate, "2006-01-02")
|
||||
bag["project.our_side"] = derefString(p.OurSide)
|
||||
bag["project.our_side_de"] = ourSideDE(derefString(p.OurSide))
|
||||
bag["project.our_side_en"] = ourSideEN(derefString(p.OurSide))
|
||||
bag["project.instance_level"] = derefString(p.InstanceLevel)
|
||||
bag["project.client_number"] = derefString(p.ClientNumber)
|
||||
bag["project.matter_number"] = derefString(p.MatterNumber)
|
||||
}
|
||||
if pt != nil {
|
||||
bag["project.proceeding.code"] = pt.Code
|
||||
bag["project.proceeding.jurisdiction"] = derefString(pt.Jurisdiction)
|
||||
|
||||
80
internal/services/submission_vars_projectless_test.go
Normal file
80
internal/services/submission_vars_projectless_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
package services
|
||||
|
||||
// Pins the project-less fill-what-we-can wiring (t-paliad-364 P3b). On a
|
||||
// draft started "Ohne Projekt" (project_id NULL) the draft still carries a
|
||||
// submission_code whose rule supplies a ProceedingTypeID. SubmissionVarsService
|
||||
// .Build's project-less branch now runs projectResolver{project:nil} +
|
||||
// captionResolver{project:nil} off that proceeding type, so the caption.* and
|
||||
// project.proceeding.* keys fill while the genuinely project-specific keys
|
||||
// (title / case_number / court) stay blank.
|
||||
//
|
||||
// This pins the resolver-set the branch assembles directly (no DB): the same
|
||||
// two nil-project resolvers, run into one bag, must produce filled caption
|
||||
// wording + proceeding name and leave the project-specific keys absent.
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"mgit.msbls.de/m/paliad/pkg/docforge"
|
||||
)
|
||||
|
||||
func TestProjectlessFill_CaptionAndProceedingFromRuleProceeding(t *testing.T) {
|
||||
// DE LG infringement proceeding — the kind a submission_code's rule
|
||||
// carries via proceeding_type_id even when no project is bound.
|
||||
pt := ptType("de.inf.lg", "DE")
|
||||
pt.Name = "Patentverletzungsklage LG"
|
||||
pt.NameEN = "Patent infringement action (LG)"
|
||||
|
||||
// Mirror exactly what the project-less branch of Build appends.
|
||||
bag := docforge.NewResolverSet(
|
||||
projectResolver{project: nil, pt: pt, lang: "de"},
|
||||
captionResolver{project: nil, pt: pt, lang: "de"},
|
||||
).BuildBag()
|
||||
|
||||
// caption.* fills from the proceeding alone (resolveCaption is nil-project
|
||||
// safe) — heading / designations / versus / subject all resolved, NOT
|
||||
// [KEIN WERT].
|
||||
wantCaption := map[string]string{
|
||||
"caption.heading": "In dem Rechtsstreit",
|
||||
"caption.claimant_designation": "Klägerin",
|
||||
"caption.defendant_designation": "Beklagte",
|
||||
"caption.versus": "gegen",
|
||||
"caption.subject": "Patentverletzung",
|
||||
}
|
||||
for k, want := range wantCaption {
|
||||
if got := bag[k]; got != want {
|
||||
t.Errorf("bag[%q] = %q, want %q (caption must fill on project-less draft)", k, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// project.proceeding.* fills from pt.
|
||||
if got := bag["project.proceeding.name"]; got == "" {
|
||||
t.Errorf("bag[\"project.proceeding.name\"] is empty; want the proceeding name filled from the rule's proceeding type")
|
||||
}
|
||||
if got := bag["project.proceeding.code"]; got != "de.inf.lg" {
|
||||
t.Errorf("bag[\"project.proceeding.code\"] = %q, want %q", got, "de.inf.lg")
|
||||
}
|
||||
|
||||
// Genuinely project-specific keys stay absent — addProjectVars skips them
|
||||
// when project is nil, so they fall through to the lawyer's overrides /
|
||||
// [KEIN WERT] rather than rendering a bogus value.
|
||||
for _, k := range []string{"project.title", "project.case_number", "project.court"} {
|
||||
if _, present := bag[k]; present {
|
||||
t.Errorf("bag[%q] present on a project-less draft; expected it to stay unset for the lawyer to fill", k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pins nil-project safety of addProjectVars directly: a nil project must not
|
||||
// panic and must still populate the proceeding namespace from pt.
|
||||
func TestAddProjectVars_NilProjectFillsProceedingOnly(t *testing.T) {
|
||||
bag := PlaceholderMap{}
|
||||
addProjectVars(bag, nil, ptType("upc.inf.cfi", "UPC"), "en")
|
||||
|
||||
if got := bag["project.proceeding.code"]; got != "upc.inf.cfi" {
|
||||
t.Errorf("project.proceeding.code = %q, want %q", got, "upc.inf.cfi")
|
||||
}
|
||||
if _, present := bag["project.title"]; present {
|
||||
t.Error("project.title present for a nil project; want it skipped")
|
||||
}
|
||||
}
|
||||
@@ -1,47 +1,63 @@
|
||||
// HL-firm skeleton submission template generator (t-paliad-275).
|
||||
// Merge-safe styled firm-skeleton generator (t-paliad-275 → t-paliad-364 P3a).
|
||||
//
|
||||
// Reads HLC's "HL Patents Style" .dotm letterhead, strips its VBA
|
||||
// macros and template-only artifacts, then emits a clean .docx that:
|
||||
// Produces the firm-formatted, MERGE-SAFE Schriftsatz skeleton paliad's
|
||||
// submission generator picks up via the merge-path fallback chain
|
||||
// (resolveSubmissionTemplate, internal/handlers/submission_drafts.go):
|
||||
//
|
||||
// 1. Preserves every HL paragraph + character style (HLpat-Heading-H1,
|
||||
// HLpat-Body-B0, HLpat-Signature, HLpat-Table-Recitals-*, …) by
|
||||
// keeping word/styles.xml, word/theme/*, word/numbering.xml,
|
||||
// word/fontTable.xml, settings.xml, footnotes/endnotes from the
|
||||
// source .dotm untouched.
|
||||
// 2. Preserves the firm letterhead (logo header + firm-address footer)
|
||||
// by keeping word/header[12].xml + word/footer[12].xml and the
|
||||
// sectPr that references them.
|
||||
// 3. Replaces word/document.xml with a Schriftsatz-shaped body that
|
||||
// exercises every SubmissionVarsService placeholder (firm.*,
|
||||
// today.*, user.*, project.*, parties.*, procedural_event.*, rule.*,
|
||||
// deadline.*) — applying HL paragraph/character styles to each
|
||||
// section so the rendered output reads as a real HL submission with
|
||||
// variables substituted.
|
||||
// - tier 4 `_firm-skeleton.docx` (DE drafts + project-less drafts)
|
||||
// - tier 3 `_skeleton.en.docx` (EN drafts)
|
||||
//
|
||||
// Drop the output into HL/mWorkRepo at
|
||||
// Both tiers are GUARDED by docx.HasMergePlaceholders: a template only
|
||||
// wins the merge path if word/document.xml carries real {{key}}
|
||||
// placeholders. The firm-skeleton's body had been repurposed into an
|
||||
// anchors-only Composer base ({{#section:KEY}} markers; t-paliad-313
|
||||
// Slice B), so the guard rejected it and every generated submission fell
|
||||
// back to the in-process docx.BuildFallbackSkeleton — a plain, generic
|
||||
// (Heading1/Normal) Rubrum (kepler diagnosis t-paliad-363 §P3a). This
|
||||
// generator restores a merge-safe firm-styled body so the guard accepts
|
||||
// it again and the resolver auto-prefers it (no handler change).
|
||||
//
|
||||
// 6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx
|
||||
// HOW: it does NOT rebuild the package from the macro-bearing .dotm.
|
||||
// Instead it takes an already-clean .docx CARRIER (the deployed
|
||||
// firm-skeleton) and replaces ONLY word/document.xml with a clean,
|
||||
// caption-driven Rubrum, preserving every other part byte-for-byte —
|
||||
// the firm styles.xml, theme, numbering, fontTable, the letterhead
|
||||
// header[12]/footer[12] + logo media, customXml, settings. The carrier's
|
||||
// own <w:document> namespaces and <w:sectPr> (which wires the letterhead
|
||||
// header/footer references) are reused verbatim, so the output keeps the
|
||||
// firm letterhead on every page.
|
||||
//
|
||||
// so paliad's submission generator picks it up via the fallback chain.
|
||||
// Lookup order after this CL: per-firm per-code → _firm-skeleton.docx
|
||||
// (THIS file — HL formatting + placeholders) → universal _skeleton.docx
|
||||
// (generic skeleton from t-paliad-259) → bare HL Patents Style .dotm
|
||||
// (no placeholders). See internal/handlers/submission_drafts.go
|
||||
// resolveSubmissionTemplate.
|
||||
// The Rubrum body MIRRORS docx.BuildFallbackSkeleton (the in-process
|
||||
// merge fallback) — same layout, same {{key}} / {{caption.*}} placeholder
|
||||
// surface — but applies the firm's named paragraph styles instead of the
|
||||
// generic Heading2/Normal: party lines → <prefix>Table-Recitals-Party /
|
||||
// PartyDetails / PartyRoles, the versus connector → Sequencers, section
|
||||
// heads → <prefix>Heading-H2, the signature block → <prefix>Signature,
|
||||
// everything else → <prefix>Body-B0.
|
||||
//
|
||||
// Why this is firm-specific: the .dotm carries HL-licensed fonts,
|
||||
// HL-branded logo media, and HLpat-prefixed style IDs. The output lives
|
||||
// under the firm-namespaced directory in mWorkRepo so a future firm gets
|
||||
// its own equivalent file generated against its own .dotm.
|
||||
// The caption wording (heading / designations / versus / subject) comes
|
||||
// from the SHARED parametric {{caption.*}} keys (t-paliad-358 A-S2), in
|
||||
// their bare (draft-language-resolved) form, so the same file renders DE
|
||||
// or EN caption wording per draft. Only the static scaffold labels
|
||||
// ("Aktenzeichen:", "wegen", …) and the today/our-side aliases are
|
||||
// language-baked — hence one file per language.
|
||||
//
|
||||
// Run:
|
||||
// Style-prefix drift: the firm style IDs are auto-detected from the
|
||||
// carrier's word/styles.xml. The originally-deployed firm-skeleton uses
|
||||
// the `HLpat-` prefix; the upstream `HLC Patents Style.dotm` was rebuilt
|
||||
// during the HL→HLC rebrand and now emits `HLCpat-`. Detecting the prefix
|
||||
// from the carrier keeps this generator correct against either source and
|
||||
// across that migration. (Reconciling the prefix across all consumers is
|
||||
// a separate follow-up — flagged in t-paliad-364.)
|
||||
//
|
||||
// go run ./scripts/gen-hl-skeleton-template \
|
||||
// -in /tmp/hl-patents-style.dotm \
|
||||
// -out /tmp/_firm-skeleton.docx
|
||||
// Run (one file per language):
|
||||
//
|
||||
// Output is byte-stable across runs for a given input (zip mtimes
|
||||
// pinned).
|
||||
// go run ./scripts/gen-hl-skeleton-template -in carrier.docx -lang de -out _firm-skeleton.docx
|
||||
// go run ./scripts/gen-hl-skeleton-template -in carrier.docx -lang en -out _skeleton.en.docx
|
||||
//
|
||||
// where carrier.docx is the deployed firm-skeleton fetched from
|
||||
// HL/mWorkRepo:6 - material/Templates/Word/Paliad/HLC/_firm-skeleton.docx.
|
||||
// Output is byte-stable across runs for a given (input, lang).
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -51,27 +67,34 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
in := flag.String("in", "", "path to source HL Patents Style .dotm (required)")
|
||||
in := flag.String("in", "", "path to the clean .docx carrier (deployed firm-skeleton) — required")
|
||||
out := flag.String("out", "_firm-skeleton.docx", "output .docx path")
|
||||
lang := flag.String("lang", "de", "draft language for the static scaffold labels: de | en")
|
||||
flag.Parse()
|
||||
|
||||
if *in == "" {
|
||||
fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: -in is required (path to HL Patents Style .dotm)")
|
||||
fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: -in is required (path to the clean .docx firm-skeleton carrier)")
|
||||
os.Exit(2)
|
||||
}
|
||||
l := strings.ToLower(strings.TrimSpace(*lang))
|
||||
if l != "de" && l != "en" {
|
||||
fmt.Fprintf(os.Stderr, "gen-hl-skeleton-template: -lang must be de or en, got %q\n", *lang)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
srcBytes, err := os.ReadFile(*in)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: read source:", err)
|
||||
fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: read carrier:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
docx, err := buildDocx(srcBytes)
|
||||
docx, err := buildDocx(srcBytes, l)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template:", err)
|
||||
os.Exit(1)
|
||||
@@ -80,89 +103,87 @@ func main() {
|
||||
fmt.Fprintln(os.Stderr, "gen-hl-skeleton-template: write:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("wrote %s (%d bytes)\n", *out, len(docx))
|
||||
fmt.Printf("wrote %s (%d bytes, lang=%s)\n", *out, len(docx), l)
|
||||
}
|
||||
|
||||
// fixedTime pins every zip entry's mtime so successive runs over the
|
||||
// same .dotm produce byte-stable output. Useful for diffing the
|
||||
// generated file in PR review.
|
||||
var fixedTime = time.Date(2026, 5, 25, 0, 0, 0, 0, time.UTC)
|
||||
// fixedTime pins every zip entry's mtime so successive runs over the same
|
||||
// (carrier, lang) produce byte-stable output. Useful for diffing the
|
||||
// generated file in review.
|
||||
var fixedTime = time.Date(2026, 6, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// dropPaths lists zip entries removed during the .dotm → .docx
|
||||
// conversion. VBA macros + their keymap binding + the template-only
|
||||
// glossary parts and ribbon customizations are all dead weight (and
|
||||
// some actively trigger Word's macro-security warning) — none of them
|
||||
// add anything to a placeholder-rich Schriftsatz starter.
|
||||
var dropPaths = map[string]bool{
|
||||
"word/vbaProject.bin": true,
|
||||
"word/vbaData.xml": true,
|
||||
"word/customizations.xml": true,
|
||||
"userCustomization/customUI.xml": true,
|
||||
"customUI/customUI14.xml": true,
|
||||
"word/glossary/document.xml": true,
|
||||
"word/glossary/_rels/document.xml.rels": true,
|
||||
"word/glossary/fontTable.xml": true,
|
||||
"word/glossary/numbering.xml": true,
|
||||
"word/glossary/settings.xml": true,
|
||||
"word/glossary/styles.xml": true,
|
||||
"word/glossary/webSettings.xml": true,
|
||||
}
|
||||
|
||||
// rIdsToDrop names the document-rel ids whose targets are stripped
|
||||
// from the package (vbaProject, customizations.xml, glossary). They
|
||||
// must vanish from word/_rels/document.xml.rels so Word doesn't choke
|
||||
// on a dangling reference.
|
||||
var rIdsToDrop = map[string]bool{
|
||||
"rId1": true, // vbaProject.bin
|
||||
"rId2": true, // customizations.xml (keymap to VBA)
|
||||
"rId21": true, // glossary/document.xml
|
||||
}
|
||||
|
||||
func buildDocx(src []byte) ([]byte, error) {
|
||||
// buildDocx copies every part of the carrier byte-for-byte except
|
||||
// word/document.xml, which is replaced with the merge-safe firm-styled
|
||||
// Rubrum for the requested language. The carrier's own <w:document> open
|
||||
// tag and <w:sectPr> are reused so the letterhead header/footer wiring is
|
||||
// preserved exactly.
|
||||
func buildDocx(src []byte, lang string) ([]byte, error) {
|
||||
zr, err := zip.NewReader(bytes.NewReader(src), int64(len(src)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open source zip: %w", err)
|
||||
return nil, fmt.Errorf("open carrier zip: %w", err)
|
||||
}
|
||||
|
||||
// Read the two parts we need to inspect: styles.xml (prefix detection)
|
||||
// and document.xml (open tag + sectPr reuse).
|
||||
var stylesXML, docXML string
|
||||
for _, f := range zr.File {
|
||||
switch f.Name {
|
||||
case "word/styles.xml":
|
||||
b, err := readZipEntry(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read word/styles.xml: %w", err)
|
||||
}
|
||||
stylesXML = string(b)
|
||||
case "word/document.xml":
|
||||
b, err := readZipEntry(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read word/document.xml: %w", err)
|
||||
}
|
||||
docXML = string(b)
|
||||
}
|
||||
}
|
||||
if stylesXML == "" {
|
||||
return nil, fmt.Errorf("carrier has no word/styles.xml")
|
||||
}
|
||||
if docXML == "" {
|
||||
return nil, fmt.Errorf("carrier has no word/document.xml")
|
||||
}
|
||||
|
||||
prefix, err := detectStylePrefix(stylesXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openTag, err := documentOpenTag(docXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sectPr, err := extractSectPr(docXML)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newDoc := buildDocumentXML(lang, prefix, openTag, sectPr)
|
||||
|
||||
var buf bytes.Buffer
|
||||
zw := zip.NewWriter(&buf)
|
||||
|
||||
for _, f := range zr.File {
|
||||
name := f.Name
|
||||
if dropPaths[name] {
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := readZipEntry(f)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", name, err)
|
||||
return nil, fmt.Errorf("read %s: %w", f.Name, err)
|
||||
}
|
||||
|
||||
switch name {
|
||||
case "[Content_Types].xml":
|
||||
body = []byte(patchContentTypes(string(body)))
|
||||
case "_rels/.rels":
|
||||
body = []byte(patchRootRels(string(body)))
|
||||
case "word/_rels/document.xml.rels":
|
||||
body = []byte(patchDocumentRels(string(body)))
|
||||
case "word/document.xml":
|
||||
body = []byte(buildDocumentXML())
|
||||
if f.Name == "word/document.xml" {
|
||||
body = []byte(newDoc)
|
||||
}
|
||||
|
||||
hdr := &zip.FileHeader{
|
||||
Name: name,
|
||||
w, err := zw.CreateHeader(&zip.FileHeader{
|
||||
Name: f.Name,
|
||||
Method: zip.Deflate,
|
||||
Modified: fixedTime,
|
||||
}
|
||||
w, err := zw.CreateHeader(hdr)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create %s: %w", name, err)
|
||||
return nil, fmt.Errorf("create %s: %w", f.Name, err)
|
||||
}
|
||||
if _, err := w.Write(body); err != nil {
|
||||
return nil, fmt.Errorf("write %s: %w", name, err)
|
||||
return nil, fmt.Errorf("write %s: %w", f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := zw.Close(); err != nil {
|
||||
return nil, fmt.Errorf("finalise zip: %w", err)
|
||||
}
|
||||
@@ -178,265 +199,203 @@ func readZipEntry(f *zip.File) ([]byte, error) {
|
||||
return io.ReadAll(rc)
|
||||
}
|
||||
|
||||
// patchContentTypes rewrites the macroEnabledTemplate part type to the
|
||||
// regular wordprocessingml.document type (a .dotm carries the macro
|
||||
// part type even on the body part), and removes Default/Override
|
||||
// entries that target now-deleted parts (vba binary, customizations,
|
||||
// glossary).
|
||||
func patchContentTypes(in string) string {
|
||||
out := in
|
||||
out = strings.ReplaceAll(out,
|
||||
`<Override PartName="/word/document.xml" ContentType="application/vnd.ms-word.template.macroEnabledTemplate.main+xml"/>`,
|
||||
`<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>`)
|
||||
|
||||
removals := []string{
|
||||
`<Default Extension="bin" ContentType="application/vnd.ms-office.vbaProject"/>`,
|
||||
`<Override PartName="/word/vbaData.xml" ContentType="application/vnd.ms-word.vbaData+xml"/>`,
|
||||
`<Override PartName="/word/customizations.xml" ContentType="application/vnd.ms-word.keyMapCustomizations+xml"/>`,
|
||||
`<Override PartName="/word/glossary/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml"/>`,
|
||||
`<Override PartName="/word/glossary/numbering.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml"/>`,
|
||||
`<Override PartName="/word/glossary/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/>`,
|
||||
`<Override PartName="/word/glossary/settings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml"/>`,
|
||||
`<Override PartName="/word/glossary/webSettings.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml"/>`,
|
||||
`<Override PartName="/word/glossary/fontTable.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml"/>`,
|
||||
}
|
||||
for _, r := range removals {
|
||||
out = strings.ReplaceAll(out, r, "")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// patchRootRels drops the userCustomization (ribbon mini-tab) and the
|
||||
// customUI14 extensibility relationships — both reference VBA-backed
|
||||
// UI we don't ship.
|
||||
func patchRootRels(in string) string {
|
||||
out := in
|
||||
out = stripRelByPrefix(out, `<Relationship Id="rId2" Type="http://schemas.microsoft.com/office/2006/relationships/ui/userCustomization"`)
|
||||
out = stripRelByPrefix(out, `<Relationship Id="Rf8f70ab1afd0469a" Type="http://schemas.microsoft.com/office/2007/relationships/ui/extensibility"`)
|
||||
return out
|
||||
}
|
||||
|
||||
// patchDocumentRels drops the document-level rels whose targets we
|
||||
// stripped (vbaProject, customizations.xml, glossaryDocument).
|
||||
func patchDocumentRels(in string) string {
|
||||
out := in
|
||||
for rid := range rIdsToDrop {
|
||||
needle := `<Relationship Id="` + rid + `" `
|
||||
out = stripRelByPrefix(out, needle)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// stripRelByPrefix removes the full <Relationship .../> element whose
|
||||
// open tag starts with the given prefix. Tolerates either a regular
|
||||
// closing tag (</Relationship>) or the more common self-closing form.
|
||||
func stripRelByPrefix(s, prefix string) string {
|
||||
for {
|
||||
start := strings.Index(s, prefix)
|
||||
if start < 0 {
|
||||
return s
|
||||
// detectStylePrefix returns the firm style-id prefix the carrier defines —
|
||||
// "HLCpat-" (current HLC Patents Style.dotm) or "HLpat-" (the originally
|
||||
// deployed firm-skeleton) — keyed off the Recitals-Party style every firm
|
||||
// Rubrum needs. Erroring out here is deliberate: a carrier missing the
|
||||
// Recitals styles would silently produce an unstyled document.
|
||||
func detectStylePrefix(stylesXML string) (string, error) {
|
||||
for _, p := range []string{"HLCpat-", "HLpat-"} {
|
||||
if strings.Contains(stylesXML, `w:styleId="`+p+`Table-Recitals-Party"`) {
|
||||
return p, nil
|
||||
}
|
||||
// Find end of this element (next "/>"). The .dotm always uses the
|
||||
// self-closing form for Relationship elements.
|
||||
end := strings.Index(s[start:], "/>")
|
||||
if end < 0 {
|
||||
return s
|
||||
}
|
||||
return "", fmt.Errorf("carrier styles.xml carries neither HLCpat-Table-Recitals-Party nor HLpat-Table-Recitals-Party — not a firm-styled skeleton")
|
||||
}
|
||||
|
||||
var (
|
||||
docOpenRegex = regexp.MustCompile(`(?s)<w:document\b[^>]*>`)
|
||||
sectPrRegex = regexp.MustCompile(`(?s)<w:sectPr\b.*</w:sectPr>`)
|
||||
)
|
||||
|
||||
// documentOpenTag returns the carrier's <w:document …> open tag verbatim so
|
||||
// the rebuilt body keeps the exact namespace declarations the sectPr (r:id
|
||||
// refs) and styles rely on.
|
||||
func documentOpenTag(docXML string) (string, error) {
|
||||
m := docOpenRegex.FindString(docXML)
|
||||
if m == "" {
|
||||
return "", fmt.Errorf("carrier document.xml has no <w:document> open tag")
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// extractSectPr returns the carrier's <w:sectPr>…</w:sectPr> block verbatim.
|
||||
// It wires the letterhead header/footer references (rId16=header1,
|
||||
// rId17=footer1, rId18=header2 first-page, rId19=footer2 first-page) and the
|
||||
// A4 page geometry; reusing it keeps the firm letterhead on every page.
|
||||
func extractSectPr(docXML string) (string, error) {
|
||||
m := sectPrRegex.FindString(docXML)
|
||||
if m == "" {
|
||||
return "", fmt.Errorf("carrier document.xml has no <w:sectPr> — cannot preserve letterhead wiring")
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// firmLabels holds the language-dependent static scaffold text. Dynamic
|
||||
// values stay as {{key}} placeholders regardless of language; the caption
|
||||
// pieces use the BARE {{caption.*}} keys (draft-language-resolved) so the
|
||||
// procedural wording flips DE/EN per draft even though the scaffold labels
|
||||
// are baked. Mirrors docx.fallbackLabelsFor so the firm-styled and
|
||||
// in-process fallbacks read identically.
|
||||
type firmLabels struct {
|
||||
editor string
|
||||
dateKey string
|
||||
caseNo string
|
||||
representedBy string
|
||||
others string
|
||||
wegen string
|
||||
subjectLabel string
|
||||
patent string
|
||||
proceeding string
|
||||
ourSideKey string
|
||||
bodyHint string
|
||||
closing string
|
||||
}
|
||||
|
||||
func labelsFor(lang string) firmLabels {
|
||||
if lang == "en" {
|
||||
return firmLabels{
|
||||
editor: "Attorney:",
|
||||
dateKey: "{{today.long_en}}",
|
||||
caseNo: "Case no.:",
|
||||
representedBy: "represented by",
|
||||
others: "Further parties:",
|
||||
wegen: "re",
|
||||
subjectLabel: "Subject",
|
||||
patent: "Patent in suit:",
|
||||
proceeding: "Proceeding:",
|
||||
ourSideKey: "{{project.our_side_en}}",
|
||||
bodyHint: "[Body of the submission goes here. This is a basic firm-styled skeleton — fill in according to the submission type.]",
|
||||
closing: "Closing",
|
||||
}
|
||||
s = s[:start] + s[start+end+2:]
|
||||
}
|
||||
return firmLabels{
|
||||
editor: "Bearbeiter:",
|
||||
dateKey: "{{today.long_de}}",
|
||||
caseNo: "Aktenzeichen:",
|
||||
representedBy: "vertreten durch",
|
||||
others: "Weitere Beteiligte:",
|
||||
wegen: "wegen",
|
||||
subjectLabel: "Betreff",
|
||||
patent: "Streitpatent:",
|
||||
proceeding: "Verfahrensart:",
|
||||
ourSideKey: "{{project.our_side_de}}",
|
||||
bodyHint: "[Hier folgt der Schriftsatztext. Diese Skelett-Vorlage trägt keine vorgefertigte Struktur — bitte gemäß Schriftsatz-Typ ergänzen.]",
|
||||
closing: "Schlussformel",
|
||||
}
|
||||
}
|
||||
|
||||
// buildDocumentXML emits a Schriftsatz skeleton that exercises every
|
||||
// SubmissionVarsService placeholder (the canonical 48-key v1 contract
|
||||
// + the procedural_event.* canonical names + their rule.* legacy
|
||||
// aliases). The structure mirrors a real DE/UPC submission — title
|
||||
// block → court → rubrum → patent reference → submission title →
|
||||
// legal grounds → Sachverhalt/Anträge/Rechtsausführungen/Beweis →
|
||||
// signature → locale-variant verification footer.
|
||||
//
|
||||
// Each placeholder lives in its own <w:r> run so the renderer's pass-1
|
||||
// (format-preserving single-run replace) catches every key. HL
|
||||
// paragraph styles (HLpat-Heading-H1, HLpat-Header-Section, etc.) are
|
||||
// applied via pStyle, character styles via rStyle.
|
||||
//
|
||||
// The sectPr at the bottom is copied verbatim from the source .dotm
|
||||
// so the firm header/footer references (rId16=header1, rId17=footer1,
|
||||
// rId18=header2 first-page, rId19=footer2 first-page) keep resolving
|
||||
// after we replace the body. pgSz/pgMar/cols/docGrid match the .dotm
|
||||
// exactly — a lawyer printing this gets the same A4 layout the .dotm
|
||||
// produces.
|
||||
func buildDocumentXML() string {
|
||||
// buildDocumentXML emits the merge-safe firm-styled Rubrum body. Layout
|
||||
// mirrors docx.buildFallbackDocumentXML (author/date → court/case/proceeding
|
||||
// → Rubrum heading → claimant block → versus → defendant block → others →
|
||||
// wegen-subject → patent → body placeholder → closing/signature) so the two
|
||||
// merge fallbacks stay structurally identical; only the paragraph styles
|
||||
// differ (firm HLpat/HLCpat styles here vs generic Heading2/Normal there).
|
||||
func buildDocumentXML(lang, prefix, openTag, sectPr string) string {
|
||||
l := labelsFor(lang)
|
||||
|
||||
body0 := prefix + "Body-B0"
|
||||
heading := prefix + "Heading-H2"
|
||||
party := prefix + "Table-Recitals-Party"
|
||||
partyDetails := prefix + "Table-Recitals-PartyDetails"
|
||||
partyRoles := prefix + "Table-Recitals-PartyRoles"
|
||||
sequencer := prefix + "Table-Recitals-Sequencers"
|
||||
signature := prefix + "Signature"
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`)
|
||||
b.WriteString(`<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">`)
|
||||
b.WriteString(openTag)
|
||||
b.WriteString(`<w:body>`)
|
||||
|
||||
skeletonBanner(&b)
|
||||
// Author / date block. The firm identity + logo live in the letterhead
|
||||
// header/footer (preserved via the carrier's sectPr), so they are not
|
||||
// repeated in the body.
|
||||
para(&b, body0, l.editor+" {{user.display_name}}")
|
||||
para(&b, body0, "{{user.email}} · {{user.office}}")
|
||||
para(&b, body0, l.dateKey)
|
||||
|
||||
heading(&b, "HLpat-Heading-H1", "{{firm.name}}")
|
||||
body0(&b, "Bearbeiter: {{user.display_name}}")
|
||||
body0(&b, "E-Mail: {{user.email}} · Büro: {{user.office}}")
|
||||
body0(&b, "Datum: {{today.long_de}} ({{today.iso}})")
|
||||
body0(&b, "{{firm.signature_block}}")
|
||||
// Court + case number + proceeding.
|
||||
para(&b, body0, "{{project.court}}")
|
||||
para(&b, body0, l.caseNo+" {{project.case_number}}")
|
||||
para(&b, body0, l.proceeding+" {{project.proceeding.name}}")
|
||||
|
||||
headerSection(&b, "{{project.court}}")
|
||||
body0(&b, "Aktenzeichen: {{project.case_number}}")
|
||||
body0(&b, "Verfahrensart: {{project.proceeding.name}} ({{project.proceeding.code}})")
|
||||
body0(&b, "Instanz: {{project.instance_level}}")
|
||||
// Rubrum heading — parametric caption wording, no outline number.
|
||||
headingNoNum(&b, heading, "{{caption.heading}}")
|
||||
|
||||
headerSubsection(&b, "In der Sache")
|
||||
// Claimant block (Recitals-Party auto-numbers it "1.").
|
||||
para(&b, party, "{{parties.claimant.name}}")
|
||||
para(&b, partyDetails, l.representedBy+" {{parties.claimant.representative}}")
|
||||
para(&b, partyRoles, "— {{caption.claimant_designation}} —")
|
||||
|
||||
recitalsParty(&b, "{{parties.claimant.name}}")
|
||||
recitalsPartyDetails(&b, "vertreten durch {{parties.claimant.representative}}")
|
||||
recitalsRoles(&b, "— Klägerin / Patentinhaberin / Anmelderin —")
|
||||
// Versus connector.
|
||||
para(&b, sequencer, "{{caption.versus}}")
|
||||
|
||||
recitalsSequencer(&b, "gegen")
|
||||
// Defendant block (Recitals-Party auto-numbers it "2.").
|
||||
para(&b, party, "{{parties.defendant.name}}")
|
||||
para(&b, partyDetails, l.representedBy+" {{parties.defendant.representative}}")
|
||||
para(&b, partyRoles, "— {{caption.defendant_designation}} —")
|
||||
|
||||
recitalsParty(&b, "{{parties.defendant.name}}")
|
||||
recitalsPartyDetails(&b, "vertreten durch {{parties.defendant.representative}}")
|
||||
recitalsRoles(&b, "— Beklagte / Einsprechende / Beschwerdegegnerin —")
|
||||
// Further parties + subject.
|
||||
para(&b, partyDetails, l.others+" {{parties.other.name}}")
|
||||
para(&b, body0, l.wegen+" {{caption.subject}}")
|
||||
|
||||
recitalsSequencer(&b, "sowie")
|
||||
// Patent in suit.
|
||||
headingNoNum(&b, heading, l.subjectLabel)
|
||||
para(&b, body0, l.patent+" {{project.patent_number}}")
|
||||
para(&b, body0, "{{project.title}} ("+l.ourSideKey+")")
|
||||
|
||||
recitalsParty(&b, "{{parties.other.name}}")
|
||||
recitalsPartyDetails(&b, "vertreten durch {{parties.other.representative}}")
|
||||
recitalsRoles(&b, "— Weitere Beteiligte —")
|
||||
// Body placeholder for the actual submission text.
|
||||
para(&b, body0, "")
|
||||
para(&b, body0, l.bodyHint)
|
||||
para(&b, body0, "")
|
||||
|
||||
headerSubsection(&b, "Betreff")
|
||||
body0(&b, "Streitpatent: {{project.patent_number}} (UPC-Schreibweise: {{project.patent_number_upc}})")
|
||||
body0(&b, "Anmeldung: {{project.filing_date}} · Erteilung: {{project.grant_date}}")
|
||||
body0(&b, "Projekttitel: {{project.title}}")
|
||||
body0(&b, "Unsere Seite: {{project.our_side_de}} ({{project.our_side}})")
|
||||
body0(&b, "Mandant: {{project.client_number}} · Matter: {{project.matter_number}}")
|
||||
body0(&b, "Internes Aktenzeichen: {{project.reference}}")
|
||||
|
||||
heading(&b, "HLpat-Heading-H1", "{{procedural_event.name}}")
|
||||
body0(&b, "(Schriftsatz-Code: {{procedural_event.code}})")
|
||||
body0(&b, "Rechtsgrundlage: {{procedural_event.legal_source_pretty}} ({{procedural_event.legal_source}})")
|
||||
body0(&b, "Typische Partei: {{procedural_event.primary_party}} · Schriftsatz-Typ: {{procedural_event.event_kind}}")
|
||||
|
||||
// t-paliad-287 — the dedicated Frist block was removed in 2026-05.
|
||||
// {{deadline.*}} placeholders stay resolvable in the variable bag
|
||||
// for custom templates that want them, but the default HL skeleton
|
||||
// no longer renders them in the submission body: the deadline is
|
||||
// internal/admin context and has no place in a court-bound document.
|
||||
|
||||
heading(&b, "HLpat-Heading-H2", "I. Sachverhalt")
|
||||
body0(&b, "[Hier folgt der Sachverhalt. Diese Vorlage ist eine Skelett-Fassung — bitte gemäß Schriftsatz-Typ ({{procedural_event.name}}) ausformulieren.]")
|
||||
|
||||
heading(&b, "HLpat-Heading-H2", "II. Anträge")
|
||||
requestsIntro(&b, "Es wird beantragt:")
|
||||
requestsLevel1(&b, "[Antrag 1 — gemäß {{procedural_event.legal_source_pretty}}]")
|
||||
requestsLevel1(&b, "[Antrag 2]")
|
||||
|
||||
heading(&b, "HLpat-Heading-H2", "III. Rechtsausführungen")
|
||||
body0(&b, "[Hier folgen die Rechtsausführungen.]")
|
||||
|
||||
heading(&b, "HLpat-Heading-H2", "IV. Beweis")
|
||||
evidenceOffering(&b, "Beweis: [Beweismittel — z. B. Anlage K1: {{project.patent_number}}]")
|
||||
|
||||
heading(&b, "HLpat-Heading-H2", "Schlussformel")
|
||||
signature(&b, "{{today.long_de}}")
|
||||
signature(&b, "")
|
||||
signature(&b, "{{user.display_name}}")
|
||||
signature(&b, "{{firm.name}}")
|
||||
|
||||
// Locale-aware verification block — exercises every EN/DE alias the
|
||||
// variable bag carries plus the rule.* legacy aliases so a lawyer
|
||||
// editing the template sees that both surfaces resolve. A real
|
||||
// submission deletes this section after sanity-checking the render.
|
||||
heading(&b, "HLpat-Heading-H3", "Locale-Varianten & Legacy-Aliase (SKELETON)")
|
||||
body1(&b, "EN long date: {{today.long_en}} · Today (bare alias): {{today}}")
|
||||
body1(&b, "Project our side (EN): {{project.our_side_en}} · Proceeding (EN): {{project.proceeding.name_en}}")
|
||||
body1(&b, "Proceeding (DE): {{project.proceeding.name_de}}")
|
||||
body1(&b, "Procedural event name (DE): {{procedural_event.name_de}} · (EN): {{procedural_event.name_en}}")
|
||||
body1(&b, "Rule legacy aliases — name: {{rule.name}}, name_de: {{rule.name_de}}, name_en: {{rule.name_en}}")
|
||||
body1(&b, "Rule legacy aliases — code: {{rule.submission_code}}, legal_source: {{rule.legal_source}}, legal_source_pretty: {{rule.legal_source_pretty}}")
|
||||
body1(&b, "Rule legacy aliases — primary_party: {{rule.primary_party}}, event_type: {{rule.event_type}}")
|
||||
|
||||
// sectPr — copied verbatim from the source .dotm. Keeps the firm
|
||||
// letterhead header (rId16=header1.xml, rId18=header2.xml first-page)
|
||||
// and the firm-address footer (rId17, rId19) on every printed page.
|
||||
b.WriteString(sectPrXML)
|
||||
// Closing / signature.
|
||||
headingNoNum(&b, heading, l.closing)
|
||||
para(&b, body0, l.dateKey)
|
||||
para(&b, signature, "{{user.display_name}}")
|
||||
para(&b, signature, "{{firm.signature_block}}")
|
||||
|
||||
// sectPr — reused verbatim from the carrier (letterhead wiring + A4
|
||||
// geometry).
|
||||
b.WriteString(sectPr)
|
||||
b.WriteString(`</w:body></w:document>`)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// sectPrXML matches the source .dotm's section properties exactly so
|
||||
// the firm header/footer refs and A4 page geometry round-trip.
|
||||
const sectPrXML = `<w:sectPr><w:headerReference w:type="default" r:id="rId16"/><w:footerReference w:type="default" r:id="rId17"/><w:headerReference w:type="first" r:id="rId18"/><w:footerReference w:type="first" r:id="rId19"/><w:pgSz w:w="11906" w:h="16838" w:code="9"/><w:pgMar w:top="567" w:right="1418" w:bottom="567" w:left="1418" w:header="284" w:footer="284" w:gutter="0"/><w:cols w:space="720"/><w:titlePg/><w:docGrid w:linePitch="286"/></w:sectPr>`
|
||||
|
||||
func skeletonBanner(b *strings.Builder) {
|
||||
b.WriteString(`<w:p><w:pPr><w:pStyle w:val="HLpat-Heading-H1"/></w:pPr><w:r><w:rPr><w:b/><w:color w:val="C00000"/></w:rPr><w:t xml:space="preserve">SKELETON — HL Patents Style mit Platzhaltern (nicht freigegeben)</w:t></w:r></w:p>`)
|
||||
// para writes one paragraph with the given paragraph style. The full line
|
||||
// (static label + any {{key}} placeholders) goes in a single run/text node;
|
||||
// the merge renderer's pass-1 substitutes each placeholder inside the node
|
||||
// in place (format-preserving), so no per-placeholder run splitting is
|
||||
// needed here.
|
||||
func para(b *strings.Builder, style, text string) {
|
||||
b.WriteString(`<w:p><w:pPr><w:pStyle w:val="`)
|
||||
b.WriteString(style)
|
||||
b.WriteString(`"/></w:pPr><w:r><w:t xml:space="preserve">`)
|
||||
b.WriteString(xmlEscape(text))
|
||||
b.WriteString(`</w:t></w:r></w:p>`)
|
||||
}
|
||||
|
||||
func heading(b *strings.Builder, style, text string) { styledPara(b, style, "", text) }
|
||||
func headerSection(b *strings.Builder, text string) { styledPara(b, "HLpat-Header-Section", "", text) }
|
||||
func headerSubsection(b *strings.Builder, text string) { styledPara(b, "HLpat-Header-Subsection", "", text) }
|
||||
func body0(b *strings.Builder, text string) { styledPara(b, "HLpat-Body-B0", "", text) }
|
||||
func body1(b *strings.Builder, text string) { styledPara(b, "HLpat-Body-B1", "", text) }
|
||||
func recitalsParty(b *strings.Builder, text string) { styledPara(b, "HLpat-Table-Recitals-Party", "", text) }
|
||||
func recitalsPartyDetails(b *strings.Builder, text string) { styledPara(b, "HLpat-Table-Recitals-PartyDetails", "", text) }
|
||||
func recitalsRoles(b *strings.Builder, text string) { styledPara(b, "HLpat-Table-Recitals-PartyRoles", "", text) }
|
||||
func recitalsSequencer(b *strings.Builder, text string) { styledPara(b, "HLpat-Table-Recitals-Sequencers", "", text) }
|
||||
func requestsIntro(b *strings.Builder, text string) { styledPara(b, "HLpat-Requests-Intro", "", text) }
|
||||
func requestsLevel1(b *strings.Builder, text string) { styledPara(b, "HLpat-Requests-Level1", "", text) }
|
||||
func evidenceOffering(b *strings.Builder, text string) { styledPara(b, "HLpat-EvidenceOffering", "", text) }
|
||||
func signature(b *strings.Builder, text string) { styledPara(b, "HLpat-Signature", "", text) }
|
||||
|
||||
// styledPara writes one paragraph with the given pStyle (paragraph
|
||||
// style id) and optional rStyle (character style applied to every run).
|
||||
// Empty style ids drop the corresponding wrapper. Placeholders inside
|
||||
// `text` are split into their own runs so the renderer's pass-1
|
||||
// single-run replace catches each one independently.
|
||||
func styledPara(b *strings.Builder, pStyle, rStyle, text string) {
|
||||
b.WriteString(`<w:p>`)
|
||||
if pStyle != "" {
|
||||
b.WriteString(`<w:pPr><w:pStyle w:val="`)
|
||||
b.WriteString(pStyle)
|
||||
b.WriteString(`"/></w:pPr>`)
|
||||
}
|
||||
for _, seg := range splitOnPlaceholders(text) {
|
||||
b.WriteString(`<w:r>`)
|
||||
if rStyle != "" {
|
||||
b.WriteString(`<w:rPr><w:rStyle w:val="`)
|
||||
b.WriteString(rStyle)
|
||||
b.WriteString(`"/></w:rPr>`)
|
||||
}
|
||||
b.WriteString(`<w:t xml:space="preserve">`)
|
||||
b.WriteString(xmlEscape(seg))
|
||||
b.WriteString(`</w:t></w:r>`)
|
||||
}
|
||||
b.WriteString(`</w:p>`)
|
||||
}
|
||||
|
||||
func splitOnPlaceholders(s string) []string {
|
||||
if s == "" {
|
||||
return []string{""}
|
||||
}
|
||||
var out []string
|
||||
for {
|
||||
open := strings.Index(s, "{{")
|
||||
if open < 0 {
|
||||
out = append(out, s)
|
||||
return out
|
||||
}
|
||||
close := strings.Index(s[open:], "}}")
|
||||
if close < 0 {
|
||||
out = append(out, s)
|
||||
return out
|
||||
}
|
||||
end := open + close + 2
|
||||
if open > 0 {
|
||||
out = append(out, s[:open])
|
||||
}
|
||||
out = append(out, s[open:end])
|
||||
s = s[end:]
|
||||
if s == "" {
|
||||
return out
|
||||
}
|
||||
}
|
||||
// headingNoNum writes a heading paragraph that suppresses the heading
|
||||
// style's auto-numbering (the firm Heading-H1/H2 styles carry a numbered
|
||||
// outline list; a Rubrum caption/section title must not render "1.1."). A
|
||||
// paragraph-level numId=0 override removes the paragraph from any list while
|
||||
// keeping the heading's font/spacing.
|
||||
func headingNoNum(b *strings.Builder, style, text string) {
|
||||
b.WriteString(`<w:p><w:pPr><w:pStyle w:val="`)
|
||||
b.WriteString(style)
|
||||
b.WriteString(`"/><w:numPr><w:ilvl w:val="0"/><w:numId w:val="0"/></w:numPr></w:pPr><w:r><w:t xml:space="preserve">`)
|
||||
b.WriteString(xmlEscape(text))
|
||||
b.WriteString(`</w:t></w:r></w:p>`)
|
||||
}
|
||||
|
||||
func xmlEscape(s string) string {
|
||||
|
||||
Reference in New Issue
Block a user