Move the full compose pipeline (anchor-pair splicing, append-before-sectPr,
hyperlink-rels patching, zip split/repack, final placeholder pass) into
pkg/docforge/docx/compose.go, decoupled from paliad's DB row types. The
engine now owns the entire .docx assembly.
New neutral types in docx:
- Carrier{Bytes, Stylemap} — the opaque base .docx, preserved
byte-for-byte outside the spliced regions (the lossless docforge
carrier for .docx).
- Section{Key, OrderIndex, Included, ContentMDDE, ContentMDEN} — the
format-neutral content input.
- Composer / NewComposer / ComposeOptions on those neutral types.
internal/services keeps SubmissionComposer + ComposeOptions as a thin
mapping wrapper (SubmissionSection -> docx.Section, Base.SectionSpec.Stylemap
+ BaseBytes -> docx.Carrier). handlers + the comprehensive compose_test are
unchanged; the test drives the wrapper end-to-end and its byte-exact OOXML
assertions pass = behaviour preserved.
Retired the slice-1 docx.XMLAttrEscape wrapper + its services forwarder:
compose now calls the local xmlAttrEscape inside the docx package.
Sequencing note: the paragraph-level neutral model (Document/Block/Slot the
PRD §3.2 sketches) is deferred to slice 6, where the authoring importer +
format exporters consume it. Building it now, ahead of any consumer, would
be speculative and risk the byte-identical guarantee for no gain (PRD §4 B3
principle). Carrier is the part of the model that earns its keep this cycle.
Verification: go build ./... clean, go vet clean, full module test green.
m/paliad#157
100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
package services
|
|
|
|
// Composer wrapper — bridges paliad's submission draft model
|
|
// (SubmissionSection + SubmissionBase) to the format-neutral docforge
|
|
// .docx composer (pkg/docforge/docx), extracted in slice 2 of the
|
|
// docforge train (t-paliad-349 / m/paliad#157).
|
|
//
|
|
// The full splice/assembly pipeline now lives in pkg/docforge/docx
|
|
// (compose.go): macro pre-pass, anchor-pair splicing, append-before-sectPr,
|
|
// hyperlink-rels patching, zip repack, and the final placeholder pass. This
|
|
// wrapper does the one thing the engine must not know about — mapping
|
|
// paliad's DB row types onto the neutral docx.Section / docx.Carrier
|
|
// inputs. Behaviour is byte-identical to the pre-extraction composer; the
|
|
// in-package compose_test still drives this wrapper end-to-end.
|
|
//
|
|
// Slice note: the paragraph-level neutral document model (Document / Block
|
|
// / Slot) the PRD §3.2 sketches lands in slice 6, where the authoring
|
|
// importer and the format exporters actually consume it. Building it now,
|
|
// ahead of any consumer, would be speculative and would put the
|
|
// byte-identical guarantee at risk for no gain (PRD §4 B3 principle:
|
|
// extractions earn their keep this cycle).
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"mgit.msbls.de/m/paliad/pkg/docforge/docx"
|
|
)
|
|
|
|
// SubmissionComposer assembles a base + a draft's sections into a final
|
|
// .docx. Stateless; safe for concurrent use.
|
|
type SubmissionComposer struct {
|
|
inner *docx.Composer
|
|
}
|
|
|
|
// NewSubmissionComposer wires the composer. The renderer is required — a
|
|
// nil renderer is a programmer error and the composer panics at
|
|
// construction.
|
|
func NewSubmissionComposer(renderer *SubmissionRenderer) *SubmissionComposer {
|
|
return &SubmissionComposer{inner: docx.NewComposer(renderer)}
|
|
}
|
|
|
|
// ComposeOptions carries the per-call composition inputs in paliad's own
|
|
// terms (SubmissionSection rows + the SubmissionBase chrome).
|
|
type ComposeOptions struct {
|
|
// Sections are the draft's section rows in display order. Included
|
|
// sections render; excluded rows are dropped. The caller is
|
|
// responsible for visibility — by the time the composer runs the rows
|
|
// have already been gated through SubmissionDraftService.Get +
|
|
// can_see_project.
|
|
Sections []SubmissionSection
|
|
|
|
// Base supplies the document chrome plus the stylemap for the MD
|
|
// walker. Must not be nil.
|
|
Base *SubmissionBase
|
|
|
|
// BaseBytes is the raw .docx bytes for the base, typically fetched
|
|
// from Gitea via the existing template cache.
|
|
BaseBytes []byte
|
|
|
|
// Lang ('de' or 'en') selects which content_md_* column the composer
|
|
// reads per section. Defaults to 'de' if empty.
|
|
Lang string
|
|
|
|
// Vars is the merged placeholder bag the renderer pass substitutes
|
|
// after assembly.
|
|
Vars PlaceholderMap
|
|
|
|
// Missing translates an unbound placeholder key into the marker the
|
|
// lawyer sees in Word.
|
|
Missing MissingPlaceholderFn
|
|
}
|
|
|
|
// Compose runs the full pipeline and returns the merged .docx bytes.
|
|
func (c *SubmissionComposer) Compose(ctx context.Context, opts ComposeOptions) ([]byte, error) {
|
|
if opts.Base == nil {
|
|
return nil, fmt.Errorf("submission compose: base required")
|
|
}
|
|
secs := make([]docx.Section, len(opts.Sections))
|
|
for i, s := range opts.Sections {
|
|
secs[i] = docx.Section{
|
|
Key: s.SectionKey,
|
|
OrderIndex: s.OrderIndex,
|
|
Included: s.Included,
|
|
ContentMDDE: s.ContentMDDE,
|
|
ContentMDEN: s.ContentMDEN,
|
|
}
|
|
}
|
|
return c.inner.Compose(ctx, docx.ComposeOptions{
|
|
Sections: secs,
|
|
Carrier: docx.Carrier{
|
|
Bytes: opts.BaseBytes,
|
|
Stylemap: opts.Base.SectionSpec.Stylemap,
|
|
},
|
|
Lang: opts.Lang,
|
|
Vars: opts.Vars,
|
|
Missing: opts.Missing,
|
|
})
|
|
}
|