diff --git a/CHANGELOG.md b/CHANGELOG.md
index 435a40a2e..121319a62 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -270,27 +270,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
diff --git a/docs/templates/v2-layered/using-templates.md b/docs/templates/v2-layered/using-templates.md
index 412edc6ca..f7e9d0547 100644
--- a/docs/templates/v2-layered/using-templates.md
+++ b/docs/templates/v2-layered/using-templates.md
@@ -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`
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/components/RoleRoutingTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/components/RoleRoutingTest.java
index 4b47faca6..f944e6251 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/cv/components/RoleRoutingTest.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/components/RoleRoutingTest.java
@@ -20,51 +20,42 @@
import static org.assertj.core.api.Assertions.assertThat;
/**
- * A CV whose headings are in the author's own language reaches the page on
- * every preset.
+ * A runtime module reaches the page on every modular template, whatever
+ * language its heading is written in — because it is a shape, not a CV
+ * meaning.
*
- *
Presets with a designed layout place sections into fixed slots, and they
- * chose what goes where by matching the heading against a list of English
- * words each kept privately. A CV headed {@code Berufserfahrung} or
- * {@code Опыт работы} 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.
- *
- * A module states its {@link SectionRole}, so the routing has an answer
- * that does not depend on the language the CV is written in. Every heading
- * here is deliberately in Russian and German: if any preset still routes by
- * keyword, its slot stays empty and this goes red.
+ * Slots that still ask for Experience or Skills only see the four
+ * hand-written section types. A {@link ModuleSection} is not a slot claim:
+ * it stays in document order (or the leftover tail) and draws through
+ * {@link com.demcha.compose.document.templates.cv.api.CvConstructor}. This
+ * suite holds that promise for headings no English keyword list contains.
*/
class RoleRoutingTest {
- private static Stream>> everyPreset() {
- return CvTemplates.all().stream().map(t -> Named.of(t.id(), t));
+ private static Stream>> modularPresets() {
+ return CvTemplates.modular().stream().map(t -> Named.of(t.id(), t));
}
@ParameterizedTest(name = "{0}")
- @MethodSource("everyPreset")
- void aCvWrittenInAnotherLanguageRendersOnEveryPreset(DocumentTemplate preset) {
+ @MethodSource("modularPresets")
+ void aCvWrittenInAnotherLanguageRendersOnEveryModularPreset(
+ DocumentTemplate preset) {
String text = CvComposedText.squashedNodes(preset, foreignLanguageCv());
- assertRendered(text, "Ведущий инженер", preset, "experience");
- assertRendered(text, "Информатика", preset, "education");
+ assertRendered(text, "Ведущий инженер", preset, "entries-dated");
+ assertRendered(text, "Информатика", preset, "entries-dated");
}
@ParameterizedTest(name = "{0}")
- @MethodSource("everyPreset")
- void aRoleRoutedModuleRendersWhateverItsKind(DocumentTemplate preset) {
- // The slots were guarded on the section's Java type as well as its
- // heading, so a module routed correctly was dropped anyway. Kinds here
- // are deliberately the "wrong" shape for the slot each role names —
- // experience as bullets, education as an inline list — because the
- // author picks the kind and the preset does not get a veto.
+ @MethodSource("modularPresets")
+ void aModuleRendersWhateverItsKind(DocumentTemplate preset) {
CvDocument doc = CvDocument.builder()
.identity(identity())
- .section(ModuleSection.builder("Berufserfahrung", SectionRole.EXPERIENCE,
+ .section(ModuleSection.builder("Berufserfahrung", SectionRole.OTHER,
CvKind.BULLETS)
.item(CvItem.of("Senior Engineer").paragraphs("Acme GmbH, 2021-2025"))
.build())
- .section(ModuleSection.builder("Kenntnisse", SectionRole.SKILLS,
+ .section(ModuleSection.builder("Kenntnisse", SectionRole.OTHER,
CvKind.INLINE_LIST)
.item(CvItem.of("Sprachen").paragraphs("Java 21", "Kotlin"))
.build())
@@ -72,23 +63,21 @@ void aRoleRoutedModuleRendersWhateverItsKind(DocumentTemplate preset
String text = CvComposedText.squashedNodes(preset, doc);
- assertRendered(text, "Senior Engineer", preset, "EXPERIENCE");
- assertRendered(text, "Java 21", preset, "SKILLS");
+ assertRendered(text, "Senior Engineer", preset, "bullets");
+ assertRendered(text, "Java 21", preset, "inline-list");
}
@Test
- void theRoleWinsOverAHeadingThatMatchesADifferentSlot() {
- // A module titled "Projects" but declared EXPERIENCE belongs where its
- // author said, not where its heading reads.
+ void aModuleIsNotClaimedByRoleOrHeading() {
List sections = List.of(
ModuleSection.builder("Projects", SectionRole.EXPERIENCE, CvKind.ENTRIES_DATED)
.item(CvItem.of("Senior Engineer").period("2021")).build());
assertThat(SectionRouter.find(sections, SectionRole.EXPERIENCE, List.of("experience")))
- .as("the role names the slot")
- .isNotNull();
+ .as("a module is not a CV meaning")
+ .isNull();
assertThat(SectionRouter.find(sections, SectionRole.PROJECTS, List.of("projects")))
- .as("...and the heading no longer claims a slot the role did not name")
+ .as("nor a heading the slot happens to recognise")
.isNull();
}
@@ -107,8 +96,8 @@ void aSectionWithoutARoleStillRoutesByItsHeading() {
.as("a hand-written section still matches by heading")
.isNotNull();
assertThat(SectionRouter.find(sections, SectionRole.OTHER, List.of("awards")))
- .as("SectionRole.OTHER claims no slot and falls through to the heading")
- .isNotNull();
+ .as("a runtime module is not claimed by heading either")
+ .isNull();
}
/**
@@ -120,7 +109,7 @@ void aSectionWithoutARoleStillRoutesByItsHeading() {
private static void assertRendered(String text, String words,
DocumentTemplate preset, String slot) {
assertThat(text)
- .as("%s must render the %s module routed by role", preset.id(), slot)
+ .as("%s must render the %s module", preset.id(), slot)
.contains(CvComposedText.squash(words));
}
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/components/SectionAllocationTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/components/SectionAllocationTest.java
index 516521788..8921bce65 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/cv/components/SectionAllocationTest.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/components/SectionAllocationTest.java
@@ -136,7 +136,7 @@ void aMissingFallbackLabelIsARejectedArgument() {
}
@Test
- void aRoleClaimTakesTheModuleThatNamedTheRole() {
+ void aModuleIsNeverClaimedAndStaysInTheLeftovers() {
ModuleSection experience = ModuleSection.builder("Опыт работы",
SectionRole.EXPERIENCE, CvKind.ENTRIES_DATED)
.item(CvItem.of("Ведущий инженер").period("2021"))
@@ -144,11 +144,11 @@ void aRoleClaimTakesTheModuleThatNamedTheRole() {
SectionAllocation allocation = SectionAllocation.of(List.of(SUMMARY, experience));
assertThat(allocation.claim(SectionRole.EXPERIENCE, List.of("experience")))
- .as("the heading matches no English keyword; the role is the answer")
- .isSameAs(experience);
+ .as("a runtime module is a shape, not a slot")
+ .isNull();
assertThat(allocation.remaining())
- .as("a role-claimed section is claimed, so it is not also a leftover")
- .doesNotContain(experience);
+ .as("so it is drawn with the leftovers, under the author's heading")
+ .contains(experience);
}
@Test
@@ -161,33 +161,28 @@ void aRoleClaimFallsBackToTheHeadingForSectionsWithoutARole() {
}
@Test
- void aDeclaredRoleIsNotClaimableByAnotherSlotsKeywords() {
- // Otherwise the experience slot takes it by role and the projects slot
- // takes it by heading, and the same module renders twice.
- ModuleSection module = ModuleSection.builder("Projects", SectionRole.EXPERIENCE,
+ void aModuleHeadingDoesNotClaimATypedSlot() {
+ ModuleSection module = ModuleSection.builder("Projects", SectionRole.OTHER,
CvKind.ENTRIES_DATED)
.item(CvItem.of("Senior Engineer").period("2021"))
.build();
SectionAllocation allocation = SectionAllocation.of(List.of(module));
assertThat(allocation.claim(SectionRole.PROJECTS, List.of("projects"))).isNull();
- assertThat(allocation.claim(SectionRole.EXPERIENCE, List.of("experience")))
- .isSameAs(module);
+ assertThat(allocation.remaining()).containsExactly(module);
}
@Test
- void aRoleClaimsAtMostOneSectionSoASecondSlotSeesTheNextOne() {
- ModuleSection first = ModuleSection.builder("Erfahrung", SectionRole.EXPERIENCE,
+ void twoModulesStayInTheLeftoversInDocumentOrder() {
+ ModuleSection first = ModuleSection.builder("Erfahrung", SectionRole.OTHER,
CvKind.ENTRIES_DATED).item(CvItem.of("First").period("2021")).build();
ModuleSection second = ModuleSection.builder("Weitere Erfahrung",
- SectionRole.EXPERIENCE, CvKind.ENTRIES_DATED)
+ SectionRole.OTHER, CvKind.ENTRIES_DATED)
.item(CvItem.of("Second").period("2019")).build();
SectionAllocation allocation = SectionAllocation.of(List.of(first, second));
- assertThat(allocation.claim(SectionRole.EXPERIENCE, List.of("experience"))).isSameAs(first);
- assertThat(allocation.claim(SectionRole.EXPERIENCE, List.of("experience")))
- .as("claiming hands each section out once")
- .isSameAs(second);
+ assertThat(allocation.claim(SectionRole.EXPERIENCE, List.of("experience"))).isNull();
+ assertThat(allocation.remaining()).containsExactly(first, second);
}
@Test
diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/SlotHeadingFidelityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/SlotHeadingFidelityTest.java
index dbe72b0a7..bb57c7d53 100644
--- a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/SlotHeadingFidelityTest.java
+++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/SlotHeadingFidelityTest.java
@@ -165,18 +165,12 @@ void theLanguageBlockKeepsItsLabelWhenItShowsPartOfASection() {
@Test
void aLanguagesModuleKeepsEveryRowEvenWhenNoneLooksLikeALanguage() {
- // The block sniffs for language-shaped rows because it also accepts a
- // wider section. A section routed here by its role is entirely
- // languages whatever its rows look like, so nothing in it may be
- // picked over.
- //
- // The first item carries a bracket and the other two do not, which is
- // the shape that used to lose them: one sniff hit was enough to
- // suppress the whole-section fallback, so English was drawn while
- // Deutsch and Українська reached no page at all — and the section
- // counted as claimed, so the leftover tail never saw them either.
- // Written without that bracket, this fixture would pass against the
- // old code too and guard nothing.
+ // A runtime module is not the language slot: it is leftover in the
+ // main column, under the heading the author wrote. The rows still
+ // have to all reach the page — the first item carries a bracket and
+ // the others do not, which is the shape that used to lose Deutsch
+ // and Українська when a claimed section was sniffed for
+ // language-shaped rows.
CvDocument doc = cv(ModuleSection.builder("Языки", SectionRole.LANGUAGES,
CvKind.INLINE_LIST)
.item(CvItem.of("English").paragraphs("(C1 advanced)"))
@@ -188,13 +182,10 @@ void aLanguagesModuleKeepsEveryRowEvenWhenNoneLooksLikeALanguage() {
assertBoundaryIsOne(text);
assertThat(text)
- .as("every row is drawn in the language block itself — before the "
- + "main column starts — under the author's own heading, each "
- + "keeping the level written next to it")
+ .as("every row reaches the page under the author's heading")
.containsSubsequence(squash("Языки"), squash("English"),
squash("Deutsch"), squash("B2"),
- squash("Українська"), squash("рідна"),
- squash(COLUMN_BOUNDARY));
+ squash("Українська"), squash("рідна"));
}
@Test
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionAllocation.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionAllocation.java
index a0a1a9d23..b2f802290 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionAllocation.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionAllocation.java
@@ -67,51 +67,20 @@ public static SectionAllocation of(List sections) {
}
/**
- * Claims the section this slot means, preferring a module that named the
- * role over one whose heading happens to match.
+ * Claims the typed section this slot's keywords match.
*
- * Headings are the fallback because a section that carries no role —
- * every hand-written one — has nothing else to be found by. A module that
- * did name a role is never claimed by a different slot's
- * keywords: it would then render in two places, which is a worse failure
- * than the one role routing exists to fix.
+ * A {@link ModuleSection} is never claimed. Runtime modules are
+ * shapes, not CV meanings, so they stay in {@link #remaining()} and
+ * draw through the constructor. The {@code role} argument is the
+ * slot's label for typed sections and does not inspect a module.
*
- * @param role the role this slot holds; {@code null} or
- * {@link SectionRole#OTHER} means "keywords only"
+ * @param role the role this slot holds; unused for modules
* @param keys candidate heading fragments
* @return the claimed section, or {@code null} when nothing matches
* @since 2.3.0
*/
public CvSection claim(SectionRole role, List keys) {
- if (role != null && role != SectionRole.OTHER) {
- for (CvSection section : sections) {
- if (claimedSections.containsKey(section)) {
- continue;
- }
- if (section instanceof ModuleSection module && module.role() == role
- && SectionLookup.hasContent(section)) {
- claimedSections.put(section, Boolean.TRUE);
- return section;
- }
- }
- }
- for (CvSection section : sections) {
- if (claimedSections.containsKey(section)) {
- continue;
- }
- if (section instanceof ModuleSection module
- && module.role() != SectionRole.OTHER) {
- continue;
- }
- String title = SectionLookup.normalize(section.title());
- for (String key : keys == null ? List.of() : keys) {
- if (title.contains(SectionLookup.normalize(key))) {
- claimedSections.put(section, Boolean.TRUE);
- return section;
- }
- }
- }
- return null;
+ return claim(keys);
}
/**
@@ -137,6 +106,9 @@ public CvSection claim(List keys) {
if (claimedSections.containsKey(section)) {
continue;
}
+ if (section instanceof ModuleSection) {
+ continue;
+ }
String title = SectionLookup.normalize(section.title());
for (String key : keys) {
if (title.contains(SectionLookup.normalize(key))) {
diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionRouter.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionRouter.java
index 7ef0048da..1542f8ce6 100644
--- a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionRouter.java
+++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionRouter.java
@@ -20,38 +20,23 @@
import java.util.List;
/**
- * Finds the section a preset's slot should hold, by what it means rather
- * than by what it is called — and hands it back in the shape that slot
- * knows how to draw.
+ * Finds the typed section a preset's slot should hold, and lowers a
+ * leftover {@link ModuleSection} to the shape that slot (or the module's
+ * own kind) knows how to draw.
*
- * Presets with a designed layout place their sections into fixed slots,
- * and they have been choosing what goes where by matching the section's
- * heading against a list of English words each preset keeps privately. That
- * works for a CV written in English by someone who used the expected
- * headings. A CV headed {@code Ausbildung} or {@code Навыки} matches
- * nothing and loses the section; so does {@code "Volunteering"}, and so does
- * a second section whose heading matched a word the first one took.
+ * Slots that still ask for Experience or Skills only see the four
+ * compile-time records, matched by heading. A runtime module is a shape,
+ * not a CV meaning — the template does not know Experience from Projects
+ * — so {@link #find} never claims one. Modules stay in document order, or
+ * in {@link SectionAllocation#remaining()}, and draw through
+ * {@link com.demcha.compose.document.templates.cv.api.CvConstructor}.
*
- * A {@link ModuleSection} carries a {@link SectionRole} because the
- * author already knew the answer, so the role is asked first and the
- * keywords are the fallback for the sections that have no role to give.
- *
- * The second half is the shape. These slots are written against a
- * particular section type — {@code if (!(section instanceof EntriesSection
- * entries)) return;} — because each draws its content its own way, and a
- * module reaching one would be dropped by that guard however well it was
- * routed. Each finder below therefore lowers a matched module to the type
- * its slot expects, so the preset draws it exactly as it draws everything
- * else. What that costs is stated per method: a module's description lines
- * are joined where the target type holds one string, and a bulleted
- * description reads as prose.
- *
- * A section that matches no slot at all is a different question, and one
- * this class cannot answer: it keeps no memory of what a previous slot took.
- * {@link SectionAllocation} does, so a preset that wants to draw the leftovers
- * claims through it and reads {@link SectionAllocation#remaining()} — with
- * {@link #naturalShape(CvSection)} for the shape those sections have no slot
- * to give them.
+ * The lowering half is for a leftover that has no slot of its own:
+ * {@link #naturalShape(CvSection)} and {@link #asEntries}, {@link #asRows},
+ * {@link #asParagraph}, {@link #asSkills} turn a module into the record
+ * a preset's existing renderer already takes. What that costs is stated
+ * per method: a module's description lines are joined where the target
+ * type holds one string, and a bulleted description reads as prose.
*
* @since 2.3.0
*/
@@ -265,18 +250,21 @@ public static CvSection naturalShape(CvSection section) {
}
/**
- * The section this slot should hold, or {@code null} when the document
- * has none: the first module whose role is the slot's, else the first
- * section whose heading matches one of the keys.
+ * The typed section this slot should hold, or {@code null} when the
+ * document has none: the first hand-written section whose heading
+ * matches one of the keys.
*
- * Role first, and only a role the author actually chose —
- * {@link SectionRole#OTHER} is what a module carries when the catalogue
- * has no name for it, so it never claims a slot and falls through to the
- * headings like any other section.
+ * A {@link ModuleSection} is never claimed here. A runtime module is
+ * a shape, not a CV meaning — the template does not know Experience
+ * from Projects — so slots that still ask for a {@link SectionRole}
+ * only see the four compile-time records. Modules stay in document
+ * order (or in {@link SectionAllocation#remaining()}) and draw through
+ * {@link com.demcha.compose.document.templates.cv.api.CvConstructor}.
*
* @param sections the document's sections for this slot's column
- * @param role the role this slot holds
- * @param keys heading fragments to fall back on
+ * @param role unused for modules; kept so existing slot call sites
+ * compile while they still name a role for typed sections
+ * @param keys heading fragments to match against typed sections
* @return the section, or {@code null} when nothing matches
*/
public static CvSection find(List sections, SectionRole role,
@@ -284,31 +272,18 @@ public static CvSection find(List sections, SectionRole role,
if (sections == null) {
return null;
}
- if (role != null && role != SectionRole.OTHER) {
- for (CvSection section : sections) {
- if (section instanceof ModuleSection module && module.role() == role
- && SectionLookup.hasContent(section)) {
- return section;
- }
- }
- }
- // The heading is the fallback, and it may not overrule a role. A module
- // declared EXPERIENCE and headed "Projects" belongs where its author put
- // it; letting the projects slot claim it by heading would render it in
- // both places, which is worse than the drop this routing exists to fix.
- return SectionLookup.firstMatching(spokenFor(sections), keys);
+ return SectionLookup.firstMatching(typedOnly(sections), keys);
}
- /** The sections a keyword slot may still claim: everything but a module that named its own role. */
- private static List spokenFor(List sections) {
- List open = new ArrayList<>(sections.size());
+ /** Hand-written sections only — a runtime module is not a slot claim. */
+ private static List typedOnly(List sections) {
+ List typed = new ArrayList<>(sections.size());
for (CvSection section : sections) {
- if (section instanceof ModuleSection module && module.role() != SectionRole.OTHER) {
- continue;
+ if (!(section instanceof ModuleSection)) {
+ typed.add(section);
}
- open.add(section);
}
- return open;
+ return typed;
}
/** The title, as markdown link syntax when the item carries a link. */
diff --git a/templates/src/test/java/com/demcha/compose/document/templates/cv/components/SectionRouterTest.java b/templates/src/test/java/com/demcha/compose/document/templates/cv/components/SectionRouterTest.java
index 8ee264a57..1fe2be6e8 100644
--- a/templates/src/test/java/com/demcha/compose/document/templates/cv/components/SectionRouterTest.java
+++ b/templates/src/test/java/com/demcha/compose/document/templates/cv/components/SectionRouterTest.java
@@ -37,21 +37,23 @@ private static List only(CvSection section) {
// -- role beats heading, heading still works ------------------------
@Test
- void aModuleIsFoundByItsRoleWhateverItsHeadingSays() {
+ void aModuleIsNotASlotClaim() {
CvSection module = ModuleSection.builder("Berufserfahrung", SectionRole.EXPERIENCE,
CvKind.ENTRIES_DATED)
.item(CvItem.of("Senior Engineer").period("2021"))
.build();
assertThat(SectionRouter.find(only(module), SectionRole.EXPERIENCE, List.of("experience")))
- .isSameAs(module);
+ .as("a runtime module is a shape, not a CV meaning")
+ .isNull();
+ assertThat(SectionRouter.find(only(module), SectionRole.PROJECTS, List.of("beruf")))
+ .as("nor is its heading a slot claim")
+ .isNull();
}
@Test
- void aHeadingNeverOverrulesADeclaredRole() {
- // Both slots would otherwise claim it — the experience slot by role and
- // the projects slot by heading — and the module would render twice.
- CvSection module = ModuleSection.builder("Projects", SectionRole.EXPERIENCE,
+ void aModuleHeadingDoesNotStealATypedSlot() {
+ CvSection module = ModuleSection.builder("Projects", SectionRole.OTHER,
CvKind.ENTRIES_DATED)
.item(CvItem.of("Senior Engineer").period("2021"))
.build();
@@ -131,11 +133,11 @@ void onlyTheStackedRowStyleCarriesALinkedTitle() {
@Test
void aPlainListOfSkillsArrivesAsSkillsNotAsCategoriesHoldingThemselves() {
- CvSection lowered = SectionRouter.skills(only(ModuleSection
+ CvSection lowered = SectionRouter.asSkills(ModuleSection
.builder("Kenntnisse", SectionRole.SKILLS, CvKind.BULLETS)
.item("Java 21")
.item("Kotlin")
- .build()), SectionRole.SKILLS, List.of("skills"));
+ .build());
assertThat(lowered).asInstanceOf(type(SkillsSection.class))
.extracting(SkillsSection::groups, org.assertj.core.api.InstanceOfAssertFactories.LIST)
@@ -149,11 +151,11 @@ void aPlainListOfSkillsArrivesAsSkillsNotAsCategoriesHoldingThemselves() {
@Test
void anItemWithADescriptionBecomesItsOwnSkillCategory() {
- CvSection lowered = SectionRouter.skills(only(ModuleSection
+ CvSection lowered = SectionRouter.asSkills(ModuleSection
.builder("Technical Skills", SectionRole.SKILLS, CvKind.INLINE_LIST)
.item(CvItem.of("Languages").paragraphs("Java 21", "Kotlin"))
.item("Docker")
- .build()), SectionRole.SKILLS, List.of("skills"));
+ .build());
SkillsSection skills = (SkillsSection) lowered;
assertThat(skills.groups()).extracting(SkillGroup::category)
@@ -164,10 +166,10 @@ void anItemWithADescriptionBecomesItsOwnSkillCategory() {
@Test
void proseJoinsEveryItemsDescriptionIntoOneBlock() {
- CvSection lowered = SectionRouter.paragraph(only(ModuleSection
+ CvSection lowered = SectionRouter.asParagraph(ModuleSection
.builder("Profile", SectionRole.SUMMARY, CvKind.PARAGRAPH)
.item(CvItem.of("first").paragraphs("Backend engineer.", "Ten years of it."))
- .build()), SectionRole.SUMMARY, List.of("summary"));
+ .build());
assertThat(lowered).asInstanceOf(type(ParagraphSection.class))
.extracting(ParagraphSection::body)
@@ -200,16 +202,14 @@ void nothingMatchingYieldsNullFromEveryFinder() {
// -- helpers ---------------------------------------------------------
private static List entriesOf(CvKind kind, CvItem item) {
- CvSection lowered = SectionRouter.entries(only(ModuleSection
- .of("Experience", SectionRole.EXPERIENCE, kind, item)),
- SectionRole.EXPERIENCE, List.of("experience"));
+ CvSection lowered = SectionRouter.asEntries(ModuleSection
+ .of("Experience", SectionRole.EXPERIENCE, kind, item));
return ((EntriesSection) lowered).entries();
}
private static CvSection rowsOf(RowStyle style, CvItem item) {
- return SectionRouter.rows(only(ModuleSection
- .of("Section", SectionRole.OTHER, CvKind.BULLETS, item)),
- SectionRole.OTHER, List.of("section"), style);
+ return SectionRouter.asRows(ModuleSection
+ .of("Section", SectionRole.OTHER, CvKind.BULLETS, item), style);
}
private static String rowBody(CvSection section) {