Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 50 additions & 38 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ follow semantic versioning; release dates are ISO 8601.
alone. `BodyStyle` decides whether a description reads as prose or as bullets, and
`SectionRole` states what a section *means* — the decision multi-column presets make
by matching headings against English keywords, which a CV headed `Ausbildung` or
`Навыки` never matches. The presets do not read the role yet; it travels with the
section now so a document built today needs no rewrite when the routing work lands.
`Навыки` never matches. The presets deliberately do **not** read the role: a module is a
shape, and inferring a CV meaning from a field the document declares is exactly what a
template has no business doing. A module reaches a slot by its **heading**, on the same
terms a hand-written section does — the heading is the author's own word for the block.
The role travels with the section for a consumer that wants it.

The existing four records are untouched and mix with modules in the same document.
A module renders through the existing components rather than beside them, so one
Expand Down Expand Up @@ -137,7 +140,7 @@ follow semantic versioning; release dates are ISO 8601.
renderers so it looks like the rest of the document; Mint Editorial draws entries through
its experience block and flattens rows to label/value lines rather than into its awards
grid, which is a two-column table and the wrong shape for a category of unknown size. `SectionAllocation` gained the four shaped claims (`entries`, `rows`,
`paragraph`, `skills`) that pair its role-first choice with `SectionRouter`'s shape
`paragraph`, `skills`) that pair its heading claim with `SectionRouter`'s shape
lowering, so a preset gets the section in the form its slot draws *and* the claim is
recorded — which is what makes `remaining()` the truth rather than a guess.
`SectionRouter.naturalShape(...)` answers the question a leftover raises and a slot never
Expand Down Expand Up @@ -270,27 +273,21 @@ follow semantic versioning; release dates are ISO 8601.
every continuation page of a dense CV, takes the rule back off to attribute the number to
it, and pins page 1's own 78pt opening either way.

- **Presets route by what a section means, not by the language it is written in.** A
preset with a designed layout places sections into fixed slots, and it chose what went
where by matching the heading against a list of English words each preset kept
privately — then guarded the slot on the section's Java type as well. A CV headed
`Ausbildung`, `Опыт работы`, or anything else in the author's own language matched
nothing: the section was dropped and the slot that wanted it rendered empty. Nothing
failed; the CV came out looking finished, one job short.

`SectionRouter` asks the module's `SectionRole` first and falls back to the headings
for the sections that carry no role — every hand-written one, and any module left as
`OTHER` — so a document of hand-written sections routes exactly as it did. A heading
may not overrule a role: a module declared `EXPERIENCE` and headed "Projects" goes where its
author put it, and the projects slot does not also claim it, which would have rendered
it twice. The router also hands each slot the section in the shape that slot draws, so
a module reaching a slot written against `EntriesSection` is no longer discarded by
the guard — the preset draws it exactly as it draws everything else, with the entry
style, rules and spacing that make it that preset. `SectionAllocation.claim` gained
the same role-first overload for the preset that allocates rather than looks up.

Nine presets and every slot they compose changed; a CV written in Russian and German
now renders on all sixteen, which `RoleRoutingTest` holds by rendering one.
- **Runtime modules are not slotted by CV meaning.** `SectionRouter.find` and
`SectionAllocation.claim` never take a `ModuleSection`. A template does not
know Experience from Projects; a module is a shape, and it stays in document
order (or the leftover tail) and draws through `CvConstructor`. Typed
sections still match by heading, so a hand-written `EntriesSection` titled
"Experience" lands in the same slot as before. `RoleRoutingTest` holds the
modular templates: a CV headed `Опыт работы` still reaches the page, because
it is a module, not because a role named the slot.

- **Typed sections still match slots by heading.** A hand-written
`EntriesSection` titled "Experience" lands where it always did. Runtime
modules do not: see the constructor-contract entry above. `SectionRouter`
still lowers a leftover module to the record a slot's renderer already
takes (`asEntries` / `asRows` / `asParagraph` / `asSkills` /
`naturalShape`), so a two-column leftover draws in the preset's own style.

- **The three column-flow presets keep the modular promise too.** `SidebarPortrait`,
`MonogramSidebar`, and `MintEditorial` declare `ModularCvTemplate`, so
Expand All @@ -307,11 +304,13 @@ follow semantic versioning; release dates are ISO 8601.
is now headed by its own name, which is both what the contract asks and easier to
read; a section that was never grouped carries a blank category and gets no label.

`kit()` returns the canonical kit for all three, and says why: a kit draws one body
style and these presets have two — the sidebar's and the main column's — with nothing
in `CvRenderKit` to say which column it is drawing into. Nothing in these templates
routes a body through `SectionDispatcher`, so the kit is what a caller outside them
would draw with, and canonical is the honest answer there.
Their `CvConstructor` methods forward to the canonical `ModuleRenderer` for the
same reason: a kit (and now a constructor method) draws one body style, and these
presets have two — the sidebar's and the main column's. Claimed modules still
lower through `SectionRouter` onto the slot's own renderer, which is why they
already come out in the preset's style. The constructor methods are the contract
a leftover or unclaimed module will use, and the canonical look is the honest
answer for a caller that is not sitting in a slot.

`ModularCvTemplateFidelityTest` grew two corrections in the process. Its item
assertions read raw composed text while its heading assertion dropped spacing, so a
Expand All @@ -321,14 +320,27 @@ follow semantic versioning; release dates are ISO 8601.
rendering style rather than a promise: it asserts every skill and the group's name
reach the page, and leaves the join to the preset drawing bars, chips, or a list.

- **A preset can draw runtime modules in its own style.** `CvRenderKit` is the three
shapes a section body reduces to — a paragraph, a label/value row, a timeline entry —
and a template hands back the kit it draws them with. The lowering from `CvItem`
stays shared, because deciding what a linked title looks like or which fields a kind
reads belongs to the model and must not be re-decided per preset; only the drawing is
the preset's. `BlueBanner`, `ClassicSerif`, and `EditorialBlue` now render modules
with their own entry and project shapes rather than the canonical ones — the
limitation the entry above left open.
- **The constructor contract is the module shapes, not CV meanings.** A template
that can be handed a runtime CV implements `CvConstructor`: one method per
`CvKind` (`paragraph`, `bullets`, `bulletsStacked`, `inlineList`, `entries`,
`entriesDated`) and no defaults. JSON (or any mapper) picks the kind; the
template draws the kind. It does not know whether the section is Experience or
a heading nobody anticipated — that knowledge is not in the contract. Adding a
kind is adding a method, and every `ModularCvTemplate` fails to compile until
it implements it. `CvConstructorKindGateTest` holds the bijection and that
every modular template declares the methods rather than inheriting a default.

The shared `ModuleRenderer` still owns which fields a kind reads, so a
template that wants the canonical look of a kind forwards to it. A template
that already had its own entry or project drawing implements the kind
methods onto that drawing (`BlueBanner`, `EditorialBlue`). `CvRenderKit`
remains the optional primitive hook underneath a kind method, not the
template contract: `ModularCvTemplate` no longer has `kit()`.

- **A preset can draw runtime modules in its own style.** `BlueBanner` and
`EditorialBlue` implement the kind methods through their own entry and
project shapes rather than the canonical ones. `ClassicSerif` is not on
`ModularCvTemplate` yet and still restyles through a private `CvRenderKit`.

### Fixed

Expand All @@ -350,7 +362,7 @@ follow semantic versioning; release dates are ISO 8601.
plain "Deutsch B2" was enough to lose the Deutsch.

The slot now claims in two steps and knows which it got. A section that is entirely
languages — routed by the role, or titled for the job — is drawn whole, whatever its
languages — titled for the job — is drawn whole, whatever its
rows look like. A wider section is still picked over, and the rows the block does not
draw are handed to the main column, which prints them under the title their author wrote
and in the place the document gave them. No row of a claimed section is dropped on
Expand Down
45 changes: 21 additions & 24 deletions docs/templates/v2-layered/using-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,22 +203,13 @@ shaped exactly like Education without a new type.
a payload standing in for parsed JSON, and its PDF is the shortest way to
see what each kind does to the same item.

`SectionRole` says what a section *means*, separately from how it
draws — and it is the first thing a preset routes on. A preset with a designed
layout places sections into fixed slots, and it used to choose what
went where by matching the heading against a list of English words:
a CV headed `Ausbildung` or `Навыки` matched nothing, so the section
was dropped and the slot that wanted it rendered empty. Give the module
a role and it lands in the right slot whatever language the CV is
written in, and whatever kind you chose to draw it with — for the roles
that preset has a slot for. `SectionRole.OTHER` names no slot, so a
module carrying it routes by heading like any other section.

A heading that matches a keyword still routes a section that has no
role — every hand-written section, and any module you left as
`SectionRole.OTHER`. What a heading may not do is overrule a role: a
module declared `EXPERIENCE` and headed "Projects" goes where you put
it, and the projects slot does not also claim it.
A runtime module is a shape, not a CV meaning. The template does not
know Experience from Projects. `SectionRole` is still on the builder
so existing call sites compile; slots do not read it. A module stays
in document order (or the leftover tail of a two-column preset) and
draws through `CvConstructor`. Hand-written sections still match slots
by heading, so an `EntriesSection` titled "Experience" lands where it
always did.

Modules and the four fixed types mix freely in one document, and both
render through the same components — a module drawn as `ENTRIES_DATED`
Expand Down Expand Up @@ -285,14 +276,20 @@ the section: Sidebar Portrait's language list, which picks the language rows out
of an "Additional Information" section, and Mint Editorial's skill-bar block,
whose group index above it already carries the author's heading.

A template also says *how* it draws through `CvRenderKit`. The shared
lowering turns a module into paragraphs, rows, and entries; the kit draws
them, so a preset with its own entry style renders your runtime module in
that style rather than the canonical one. Presets whose bodies already use
the shared components return `CvRenderKit.defaults()` — and so do the three
column-flow presets, which never route a body through the kit at all: each
lowers a module to the shape its slot draws and lets that slot's own renderer
draw it, which is why a runtime module already comes out in their style.
A template also says *how* it draws through `CvConstructor`: one method per
`CvKind` (`paragraph`, `bullets`, `bulletsStacked`, `inlineList`, `entries`,
`entriesDated`), no defaults. JSON picks the kind; the template implements
the kind. It does not know whether the section is Experience or a heading
the author invented. Adding a kind is adding a method, and every modular
template has to draw it.

The shared `ModuleRenderer` is what a template forwards to when it wants
the canonical look of that kind. A preset with its own entry style
implements the kind methods itself (or forwards through a `CvRenderKit` of
primitives) so a runtime module takes that style rather than the canonical
one. The three column-flow presets still lower a claimed module to the
shape their slot already draws; their constructor methods are the same
contract, ready for a leftover or unclaimed module that has no slot.

---

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.demcha.compose.document.templates.cv.api;

import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.templates.api.DocumentTemplate;
import com.demcha.compose.document.templates.core.theme.BrandTheme;
import com.demcha.compose.document.templates.cv.CvComposedText;
import com.demcha.compose.document.templates.cv.data.CvDocument;
import com.demcha.compose.document.templates.cv.data.CvIdentity;
Expand Down Expand Up @@ -164,10 +166,19 @@ void aSidebarSectionIsNotRenderedAndTheContractSaysSo(ModularCvTemplate template

@ParameterizedTest(name = "{0}")
@MethodSource("modularTemplates")
void everyModularTemplateDeclaresAKit(ModularCvTemplate template) {
assertThat(template.kit())
.as("%s must hand back a kit — the drawing half of the promise", template.id())
.isNotNull();
void everyModularTemplateImplementsEveryKind(ModularCvTemplate template)
throws NoSuchMethodException {
Class<?> type = template.getClass();
for (CvKind kind : CvKind.values()) {
assertThat(type.getMethod(
CvConstructor.methodName(kind),
SectionBuilder.class,
ModuleSection.class,
BrandTheme.class)
.getDeclaringClass())
.as("%s must implement %s", template.id(), kind)
.isNotEqualTo(CvConstructor.class);
}
}

@Test
Expand Down
Loading
Loading