From bedbecb7a63343c03c3e08d21e72ab37963dc564 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sat, 22 Aug 2026 21:36:56 +0100 Subject: [PATCH] feat(templates): the constructor contract is the module kinds A modular template implements one method per CvKind, with no defaults. JSON picks the shape; the template draws the shape. It does not know whether the section is Experience. Adding a kind is adding a method, and every ModularCvTemplate has to implement it. --- CHANGELOG.md | 41 ++-- docs/templates/v2-layered/using-templates.md | 22 ++- .../cv/api/ModularCvTemplateFidelityTest.java | 19 +- .../templates/cv/api/CvConstructor.java | 132 +++++++++++++ .../templates/cv/api/ModularCvTemplate.java | 34 ++-- .../templates/cv/components/CvRenderKit.java | 28 +-- .../cv/components/ModuleRenderer.java | 181 ++++++++++++++++-- .../cv/components/SectionDispatcher.java | 35 +++- .../templates/cv/presets/BlueBanner.java | 45 +++-- .../templates/cv/presets/BoxedSections.java | 37 +++- .../cv/presets/CenteredHeadline.java | 35 +++- .../templates/cv/presets/EditorialBlue.java | 52 +++-- .../templates/cv/presets/Executive.java | 35 +++- .../cv/presets/MinimalUnderlined.java | 37 +++- .../templates/cv/presets/MintEditorial.java | 45 +++-- .../cv/presets/ModernProfessional.java | 37 +++- .../templates/cv/presets/MonogramSidebar.java | 43 +++-- .../templates/cv/presets/SidebarPortrait.java | 45 +++-- .../cv/api/CvConstructorKindGateTest.java | 72 +++++++ 19 files changed, 793 insertions(+), 182 deletions(-) create mode 100644 templates/src/main/java/com/demcha/compose/document/templates/cv/api/CvConstructor.java create mode 100644 templates/src/test/java/com/demcha/compose/document/templates/cv/api/CvConstructorKindGateTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 89abc2707..435a40a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -307,11 +307,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 @@ -321,14 +323,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 diff --git a/docs/templates/v2-layered/using-templates.md b/docs/templates/v2-layered/using-templates.md index 638fecd6c..412edc6ca 100644 --- a/docs/templates/v2-layered/using-templates.md +++ b/docs/templates/v2-layered/using-templates.md @@ -285,14 +285,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. --- diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplateFidelityTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplateFidelityTest.java index 6ad66b8f5..a7c988849 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplateFidelityTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplateFidelityTest.java @@ -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; @@ -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 diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/api/CvConstructor.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/api/CvConstructor.java new file mode 100644 index 000000000..bc4b1bbb4 --- /dev/null +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/api/CvConstructor.java @@ -0,0 +1,132 @@ +package com.demcha.compose.document.templates.cv.api; + +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.templates.core.theme.BrandTheme; +import com.demcha.compose.document.templates.cv.data.CvKind; +import com.demcha.compose.document.templates.cv.data.ModuleSection; + +import java.util.Objects; + +/** + * The constructor contract: one method per module shape, not per + * CV meaning. + * + *

A template that implements this does not know whether a section is + * Experience, Projects, or a heading the author invented. It knows how + * to draw prose, a bullet list, an inline list, and a timeline — with + * and without dates. JSON (or any runtime mapper) picks the kind; the + * template implements the kind.

+ * + *

There are no defaults on the kind methods. Adding a {@link CvKind} + * constant adds a method here, and every {@link ModularCvTemplate} + * fails to compile until it draws the new shape. That is the point: a + * constructor surface that grows in one place and pulls every template + * with it, rather than a shared renderer that can absorb a new kind + * without the templates noticing.

+ * + *

{@link #render(SectionBuilder, ModuleSection, BrandTheme)} is the + * dispatcher, and it is a default because it is not a shape. Its + * {@code switch} is exhaustive over {@link CvKind}, so a new constant + * without a method is a compile error here too.

+ * + * @since 2.3.0 + */ +public interface CvConstructor { + + /** + * The method on this interface that draws {@code kind}. + * + *

Kept next to the methods themselves so a test can prove the + * bijection without copying the names.

+ * + * @param kind a module shape + * @return the method name, such as {@code "entriesDated"} + */ + static String methodName(CvKind kind) { + Objects.requireNonNull(kind, "kind"); + return switch (kind) { + case PARAGRAPH -> "paragraph"; + case BULLETS -> "bullets"; + case BULLETS_STACKED -> "bulletsStacked"; + case INLINE_LIST -> "inlineList"; + case ENTRIES -> "entries"; + case ENTRIES_DATED -> "entriesDated"; + }; + } + + /** + * Dispatches {@code module} to the kind method the author picked. + * + * @param host host section receiving the body + * @param module the module; its {@link ModuleSection#kind() kind} selects + * the method + * @param theme the active theme + */ + default void render(SectionBuilder host, ModuleSection module, BrandTheme theme) { + Objects.requireNonNull(host, "host"); + Objects.requireNonNull(module, "module"); + Objects.requireNonNull(theme, "theme"); + switch (module.kind()) { + case PARAGRAPH -> paragraph(host, module, theme); + case BULLETS -> bullets(host, module, theme); + case BULLETS_STACKED -> bulletsStacked(host, module, theme); + case INLINE_LIST -> inlineList(host, module, theme); + case ENTRIES -> entries(host, module, theme); + case ENTRIES_DATED -> entriesDated(host, module, theme); + } + } + + /** + * Prose under the section heading. Reads each item's body only. + * + * @param host host section receiving the body + * @param module a {@link CvKind#PARAGRAPH} module + * @param theme the active theme + */ + void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme); + + /** + * A bullet per item, description on the same line. + * + * @param host host section receiving the body + * @param module a {@link CvKind#BULLETS} module + * @param theme the active theme + */ + void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme); + + /** + * A bullet per item, description stacked underneath. + * + * @param host host section receiving the body + * @param module a {@link CvKind#BULLETS_STACKED} module + * @param theme the active theme + */ + void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme); + + /** + * One line per item, the description collapsed after a bold label. + * + * @param host host section receiving the body + * @param module a {@link CvKind#INLINE_LIST} module + * @param theme the active theme + */ + void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme); + + /** + * Timeline entries without the date column. + * + * @param host host section receiving the body + * @param module a {@link CvKind#ENTRIES} module + * @param theme the active theme + */ + void entries(SectionBuilder host, ModuleSection module, BrandTheme theme); + + /** + * Timeline entries with the date column. + * + * @param host host section receiving the body + * @param module a {@link CvKind#ENTRIES_DATED} module + * @param theme the active theme + */ + void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme); +} diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplate.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplate.java index 0d384a93b..a76428364 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplate.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/api/ModularCvTemplate.java @@ -1,17 +1,23 @@ package com.demcha.compose.document.templates.cv.api; import com.demcha.compose.document.templates.api.DocumentTemplate; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; import com.demcha.compose.document.templates.cv.data.CvDocument; import com.demcha.compose.document.templates.cv.data.CvKind; -import com.demcha.compose.document.templates.cv.data.ModuleSection; import com.demcha.compose.document.templates.cv.data.Slot; /** - * A CV template that renders every section placed in {@link Slot#MAIN} — - * every {@link CvKind}, under whatever heading the author wrote. + * A CV template that implements every constructor shape and renders every + * section placed in {@link Slot#MAIN} — every {@link CvKind}, under + * whatever heading the author wrote. * - *

The promise is exactly that, and the slot is part of it. + *

The constructor half is {@link CvConstructor}: one method per module + * shape, no defaults. A JSON mapper (or any runtime assembler) picks a + * kind; this template draws that kind. It does not know whether the + * section is Experience or a heading nobody anticipated — that knowledge + * is not in the contract.

+ * + *

The placement promise is exactly {@link Slot#MAIN}, and the + * slot is part of it. * Every shipped preset reads {@code sectionsIn(Slot.MAIN)} and no other * slot — including the ones that compose a sidebar of their own, which fill * it from the identity and from the main-slot sections their routing sends @@ -40,22 +46,10 @@ * last because a preset with an editorial vocabulary of its own is the one * likely to rename what the author wrote. Each item must reach the page, so * the interface cannot be worn by a template that would drop or retitle - * one.

- * - *

{@link #kit()} is how the promise stays compatible with a preset's own - * look: the shared lowering turns a {@link ModuleSection} into paragraphs, - * rows, and entries, and the kit draws them the way this template draws - * everything else.

+ * one. {@code CvConstructorKindGateTest} holds the other half: every kind + * has a non-default method, and every modular template declares it.

* * @since 2.3.0 */ -public interface ModularCvTemplate extends DocumentTemplate { - - /** - * How this template draws the shapes a module lowers to. - * - * @return this template's kit; {@link CvRenderKit#defaults()} for a - * template whose modules look like the canonical components - */ - CvRenderKit kit(); +public interface ModularCvTemplate extends DocumentTemplate, CvConstructor { } diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/CvRenderKit.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/CvRenderKit.java index 5391265ef..50aa0c4c6 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/CvRenderKit.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/CvRenderKit.java @@ -3,33 +3,21 @@ import com.demcha.compose.document.dsl.SectionBuilder; import com.demcha.compose.document.templates.core.theme.BrandTheme; import com.demcha.compose.document.templates.cv.data.CvEntry; -import com.demcha.compose.document.templates.cv.data.CvItem; import com.demcha.compose.document.templates.cv.data.CvKind; import com.demcha.compose.document.templates.cv.data.CvRow; import com.demcha.compose.document.templates.cv.data.RowStyle; /** - * How one template draws the three shapes a CV section body reduces to: + * Primitive drawing a {@link ModuleRenderer} kind method can restyle: * a paragraph of prose, a label/value row, a timeline entry. * - *

A preset that wants runtime {@code ModuleSection}s to look like the - * rest of its own document implements this and hands it back through - * {@link com.demcha.compose.document.templates.cv.api.ModularCvTemplate}; - * {@link #defaults()} draws them the canonical way, and every method has a - * default, so a preset overrides only the shapes it actually styles - * differently.

- * - *

Why the primitives and not the kinds. The obvious - * alternative is a function per {@link CvKind}. It puts the wrong work on - * the preset: turning a {@link CvItem} into an entry or a row means - * deciding what a linked title looks like, how a subtitle and a location - * join, which fields the kind ignores, what an empty description does to a - * trailing colon — rules that belong to the model and must not be - * re-decided sixteen times. {@link ModuleRenderer} keeps that lowering and - * asks the kit only to draw what came out of it, which is exactly the part - * a preset has an opinion about. It is also the shape the presets already - * have: their private renderers take a {@code CvEntry} or a {@code CvRow} - * today.

+ *

This is not the template contract. A modular template implements + * {@link com.demcha.compose.document.templates.cv.api.CvConstructor} — + * one method per {@link CvKind}. The kit is the optional hook underneath + * a kind method that wants its own entry or row look without re-deciding + * which fields the kind reads. {@link #defaults()} is the canonical + * drawing; every method has a default, so a preset overrides only the + * primitives it actually styles differently.

* *

Implementations draw into the host and return; they do not set the * host's spacing or padding, which the caller has already settled, and diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/ModuleRenderer.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/ModuleRenderer.java index 9d2a52726..c884f12a4 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/ModuleRenderer.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/ModuleRenderer.java @@ -52,10 +52,11 @@ public static void render(SectionBuilder host, ModuleSection module, BrandTheme * Renders every item of {@code module} into {@code host}, drawing * through {@code kit}. * - *

The lowering below is the same whoever draws: which fields a kind - * reads, how a linked title is spelled, what an empty description does - * to a trailing colon. Only the three drawing calls go to the kit, so a - * preset can restyle its modules without re-deciding any of that.

+ *

Prefers the kind methods below. A template that implements + * {@code CvConstructor} should call those (or {@code render} on + * itself) rather than this overload: this one exists so a kit can + * restyle the three drawing primitives without re-deciding which + * fields a kind reads.

* * @param host host section receiving the body * @param module the module supplying items, kind, and role @@ -64,17 +65,173 @@ public static void render(SectionBuilder host, ModuleSection module, BrandTheme */ public static void render(SectionBuilder host, ModuleSection module, BrandTheme theme, CvRenderKit kit) { + switch (module.kind()) { + case PARAGRAPH -> paragraph(host, module, theme, kit); + case BULLETS -> bullets(host, module, theme, kit); + case BULLETS_STACKED -> bulletsStacked(host, module, theme, kit); + case INLINE_LIST -> inlineList(host, module, theme, kit); + case ENTRIES -> entries(host, module, theme, kit); + case ENTRIES_DATED -> entriesDated(host, module, theme, kit); + } + } + + /** + * Canonical {@link com.demcha.compose.document.templates.cv.data.CvKind#PARAGRAPH}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + */ + public static void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + paragraph(host, module, theme, CvRenderKit.defaults()); + } + + /** + * {@link com.demcha.compose.document.templates.cv.data.CvKind#PARAGRAPH} drawn through {@code kit}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + * @param kit drawing primitives + */ + public static void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme, + CvRenderKit kit) { + for (CvItem item : module.items()) { + paragraph(host, item, theme, kit); + } + } + + /** + * Canonical {@link com.demcha.compose.document.templates.cv.data.CvKind#BULLETS}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + */ + public static void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + bullets(host, module, theme, CvRenderKit.defaults()); + } + + /** + * {@link com.demcha.compose.document.templates.cv.data.CvKind#BULLETS} drawn through {@code kit}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + * @param kit drawing primitives + */ + public static void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme, + CvRenderKit kit) { + for (CvItem item : module.items()) { + bullet(host, item, theme, kit); + } + } + + /** + * Canonical {@link com.demcha.compose.document.templates.cv.data.CvKind#BULLETS_STACKED}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + */ + public static void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + bulletsStacked(host, module, theme, CvRenderKit.defaults()); + } + + /** + * {@link com.demcha.compose.document.templates.cv.data.CvKind#BULLETS_STACKED} drawn through {@code kit}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + * @param kit drawing primitives + */ + public static void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme, + CvRenderKit kit) { + List items = module.items(); + for (int i = 0; i < items.size(); i++) { + stackedBullet(host, items.get(i), theme, kit, i > 0); + } + } + + /** + * Canonical {@link com.demcha.compose.document.templates.cv.data.CvKind#INLINE_LIST}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + */ + public static void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + inlineList(host, module, theme, CvRenderKit.defaults()); + } + + /** + * {@link com.demcha.compose.document.templates.cv.data.CvKind#INLINE_LIST} drawn through {@code kit}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + * @param kit drawing primitives + */ + public static void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme, + CvRenderKit kit) { + for (CvItem item : module.items()) { + inlineList(host, item, theme, kit); + } + } + + /** + * Canonical {@link com.demcha.compose.document.templates.cv.data.CvKind#ENTRIES}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + */ + public static void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + entries(host, module, theme, CvRenderKit.defaults()); + } + + /** + * {@link com.demcha.compose.document.templates.cv.data.CvKind#ENTRIES} drawn through {@code kit}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + * @param kit drawing primitives + */ + public static void entries(SectionBuilder host, ModuleSection module, BrandTheme theme, + CvRenderKit kit) { + List items = module.items(); + for (int i = 0; i < items.size(); i++) { + entry(host, items.get(i), "", theme, kit, i > 0); + } + } + + /** + * Canonical {@link com.demcha.compose.document.templates.cv.data.CvKind#ENTRIES_DATED}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + */ + public static void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + entriesDated(host, module, theme, CvRenderKit.defaults()); + } + + /** + * {@link com.demcha.compose.document.templates.cv.data.CvKind#ENTRIES_DATED} drawn through {@code kit}. + * + * @param host host section receiving the body + * @param module the module + * @param theme the active theme + * @param kit drawing primitives + */ + public static void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme, + CvRenderKit kit) { List items = module.items(); for (int i = 0; i < items.size(); i++) { CvItem item = items.get(i); - switch (module.kind()) { - case PARAGRAPH -> paragraph(host, item, theme, kit); - case BULLETS -> bullet(host, item, theme, kit); - case BULLETS_STACKED -> stackedBullet(host, item, theme, kit, i > 0); - case INLINE_LIST -> inlineList(host, item, theme, kit); - case ENTRIES -> entry(host, item, "", theme, kit, i > 0); - case ENTRIES_DATED -> entry(host, item, item.period(), theme, kit, i > 0); - } + entry(host, item, item.period(), theme, kit, i > 0); } } diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionDispatcher.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionDispatcher.java index b450b8e67..36bd22b85 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionDispatcher.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/components/SectionDispatcher.java @@ -1,6 +1,7 @@ package com.demcha.compose.document.templates.cv.components; import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.templates.cv.api.CvConstructor; import com.demcha.compose.document.templates.cv.data.*; import com.demcha.compose.document.templates.core.theme.BrandTheme; @@ -37,13 +38,41 @@ public static void renderBody(SectionBuilder host, CvSection section, BrandTheme renderBody(host, section, theme, CvRenderKit.defaults()); } + /** + * Renders the section body, sending a runtime module through + * {@code constructor} so each kind lands on the method the template + * implemented for it. + * + *

Typed sections still take the canonical path: the constructor + * is the module contract, not a second dispatcher for + * {@code EntriesSection}.

+ * + * @param host host section receiving the body + * @param section the section whose subtype selects the renderer + * @param theme the active theme supplying palette, typography, and spacing + * @param constructor the template's kind methods + * @throws IllegalStateException if the section subtype is unhandled + * @since 2.3.0 + */ + public static void renderBody(SectionBuilder host, CvSection section, BrandTheme theme, + CvConstructor constructor) { + if (section instanceof ModuleSection module) { + host.spacing(theme.spacing().sectionBodySpacing()) + .padding(theme.spacing().sectionBodyPadding()); + constructor.render(host, module, theme); + return; + } + renderBody(host, section, theme); + } + /** * Renders the section body, drawing through {@code kit}. * *

The routing is identical to the three-argument form; only who draws - * differs. A preset with its own entry or row style passes its kit here - * so a runtime module looks like the rest of its document instead of - * like the canonical components.

+ * differs. Prefer the {@link CvConstructor} overload for a modular + * template: that is the kind contract. This overload remains for a + * preset that is not yet on that contract and restyles the three + * drawing primitives.

* * @param host host section receiving the body * @param section the section whose subtype selects the renderer diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java index 69e7e90c5..d22c5d1d9 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BlueBanner.java @@ -10,6 +10,7 @@ import com.demcha.compose.document.style.DocumentTextDecoration; import com.demcha.compose.document.style.DocumentTextStyle; import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.cv.api.CvConstructor; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; import com.demcha.compose.document.templates.cv.components.*; import com.demcha.compose.document.templates.cv.data.*; @@ -107,8 +108,33 @@ public String displayName() { } @Override - public CvRenderKit kit() { - return KIT; + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme, KIT); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme, KIT); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme, KIT); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme, KIT); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme, KIT); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme, KIT); } @Override @@ -143,7 +169,7 @@ public void compose(DocumentSession document, CvDocument doc) { new DocumentInsets(1, BANNER_RULE_HORIZONTAL_INSET, 1, BANNER_RULE_HORIZONTAL_INSET)); pageFlow.addSection("BlueBannerBody_" + idx, host -> - renderBody(host, sec, theme)); + renderBody(host, sec, theme, this)); } pageFlow.build(); @@ -152,7 +178,8 @@ public void compose(DocumentSession document, CvDocument doc) { private static void renderBody(SectionBuilder host, CvSection section, - BrandTheme theme) { + BrandTheme theme, + CvConstructor constructor) { host.spacing(theme.spacing().sectionBodySpacing()) .padding(theme.spacing().sectionBodyPadding()); @@ -166,14 +193,10 @@ private static void renderBody(SectionBuilder host, for (CvEntry entry : e.entries()) { renderEntry(host, entry, theme); } + } else if (section instanceof ModuleSection module) { + constructor.render(host, module, theme); } else { - // A shape this preset has no styled path for — today the runtime - // ModuleSection. Hand it to the canonical dispatcher rather than - // throwing: a section the author put in the document reaches the - // page, which matters more than matching this preset's flavour of - // entry. A preset that wants its own module styling overrides this - // branch, it does not lose the content by omission. - SectionDispatcher.renderBody(host, section, theme, KIT); + SectionDispatcher.renderBody(host, section, theme); } } diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java index 269ac35c7..ac728cbaf 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/BoxedSections.java @@ -2,12 +2,14 @@ import com.demcha.compose.document.api.DocumentSession; import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; import com.demcha.compose.document.templates.api.DocumentTemplate; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionDispatcher; import com.demcha.compose.document.templates.cv.data.CvDocument; import com.demcha.compose.document.templates.cv.data.CvSection; +import com.demcha.compose.document.templates.cv.data.ModuleSection; import com.demcha.compose.document.templates.cv.data.Slot; import com.demcha.compose.document.templates.core.theme.BrandTheme; import com.demcha.compose.document.templates.core.identity.ContactLine; @@ -98,10 +100,33 @@ public String displayName() { } @Override - public CvRenderKit kit() { - // This preset renders bodies through the shared dispatcher, so a - // runtime module already looks like the rest of its document. - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override @@ -137,7 +162,7 @@ public void compose(DocumentSession document, CvDocument doc) { SectionHeader.banner(host, sec.title(), theme); }); pageFlow.addSection("CvV2Body_" + idx, - host -> SectionDispatcher.renderBody(host, sec, theme, kit())); + host -> SectionDispatcher.renderBody(host, sec, theme, this)); } pageFlow.build(); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java index bb501bd66..f7d09d33a 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/CenteredHeadline.java @@ -10,7 +10,7 @@ import com.demcha.compose.document.templates.api.DocumentTemplate; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; import com.demcha.compose.document.templates.cv.components.ProjectRenderer; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionDispatcher; import com.demcha.compose.document.templates.cv.data.*; import com.demcha.compose.document.templates.core.theme.BrandTheme; @@ -124,10 +124,33 @@ public String displayName() { } @Override - public CvRenderKit kit() { - // This preset renders bodies through the shared dispatcher, so a - // runtime module already looks like the rest of its document. - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override @@ -193,7 +216,7 @@ private void renderBody(SectionBuilder host, CvSection sec) { } return; } - SectionDispatcher.renderBody(host, sec, theme, kit()); + SectionDispatcher.renderBody(host, sec, theme, this); } private void renderStackedProject(SectionBuilder host, CvRow row) { diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java index c8cb9efe9..ad7935ead 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/EditorialBlue.java @@ -153,7 +153,7 @@ private void renderSectionBody(SectionBuilder section, CvSection cvSection, // would render as nothing at all: an empty heading over blank // space, which reads as a finished CV that quietly lost a // section. - SectionDispatcher.renderBody(section, cvSection, theme, kit()); + SectionDispatcher.renderBody(section, cvSection, theme, this); } } @@ -174,18 +174,48 @@ private void renderEntries(SectionBuilder section, EntriesSection entries) { } /** - * This preset's own drawing, so a runtime module gets the - * editorial entry, project, and key/value shapes. - * - *

Entries take the experience styling. The preset picks - * between its experience and education variants by sniffing a - * section's heading, which is exactly what a module carries a - * role to avoid; until the kit is handed that role, one of the - * two has to be the answer, and experience is the shape most - * modules take.

+ * Constructor kinds. Each one lowers through {@link ModuleRenderer} + * onto this preset's editorial drawing, so a runtime module is a + * shape, not a CV meaning — {@code ENTRIES_DATED} is the + * experience-styled timeline whether the heading says + * Experience or something else. */ @Override - public CvRenderKit kit() { + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme, drawing()); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme, drawing()); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme, drawing()); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme, drawing()); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme, drawing()); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme, drawing()); + } + + /** + * Primitive drawing this preset already had for entries, projects, + * and key/value rows. Kind methods lower through {@link ModuleRenderer} + * onto these, so a runtime module takes the editorial shapes. + */ + private CvRenderKit drawing() { return new CvRenderKit() { @Override diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java index 7842cdabf..fe644dc56 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/Executive.java @@ -14,7 +14,7 @@ import com.demcha.compose.document.templates.api.DocumentTemplate; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; import com.demcha.compose.document.templates.core.text.TextStyles; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionDispatcher; import com.demcha.compose.document.templates.cv.data.*; import com.demcha.compose.document.templates.core.theme.BrandTheme; @@ -111,10 +111,33 @@ public String displayName() { } @Override - public CvRenderKit kit() { - // This preset renders bodies through the shared dispatcher, so a - // runtime module already looks like the rest of its document. - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override @@ -141,7 +164,7 @@ public void compose(DocumentSession document, CvDocument doc) { ACCENT, theme); }); flow.addSection("CvV2ExecutiveBody_" + idx, host -> - SectionDispatcher.renderBody(host, sec, theme, kit())); + SectionDispatcher.renderBody(host, sec, theme, this)); } flow.build(); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MinimalUnderlined.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MinimalUnderlined.java index 720fa0820..ffa0502b7 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MinimalUnderlined.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MinimalUnderlined.java @@ -2,12 +2,14 @@ import com.demcha.compose.document.api.DocumentSession; import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; import com.demcha.compose.document.templates.api.DocumentTemplate; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionDispatcher; import com.demcha.compose.document.templates.cv.data.CvDocument; import com.demcha.compose.document.templates.cv.data.CvSection; +import com.demcha.compose.document.templates.cv.data.ModuleSection; import com.demcha.compose.document.templates.cv.data.Slot; import com.demcha.compose.document.templates.core.theme.BrandTheme; import com.demcha.compose.document.templates.core.identity.ContactLine; @@ -98,10 +100,33 @@ public String displayName() { } @Override - public CvRenderKit kit() { - // This preset renders bodies through the shared dispatcher, so a - // runtime module already looks like the rest of its document. - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override @@ -132,7 +157,7 @@ public void compose(DocumentSession document, CvDocument doc) { SectionHeader.underlined(host, sec.title(), theme); }); pageFlow.addSection("Body_" + idx, host -> - SectionDispatcher.renderBody(host, sec, theme, kit())); + SectionDispatcher.renderBody(host, sec, theme, this)); } pageFlow.build(); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java index 8f52e5053..3bc8d2b6d 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MintEditorial.java @@ -19,7 +19,7 @@ import com.demcha.compose.document.templates.core.text.TextStyles; import com.demcha.compose.document.templates.core.text.MarkdownInline; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionAllocation; import com.demcha.compose.document.templates.cv.components.SectionRouter; import com.demcha.compose.document.templates.cv.components.SectionLookup; @@ -449,23 +449,34 @@ public String displayName() { return DISPLAY_NAME; } - /** - * The canonical kit, and this template never consults it. - * - *

A kit is how a preset styles the bodies it routes through - * {@code SectionDispatcher}. This one routes none: a module is lowered - * by {@code SectionRouter} to the shape its slot draws, and the slot's - * own renderer draws it — which is why a runtime module already comes - * out in this preset's style rather than the canonical one. The kit is - * the value an outside caller would draw with, and the canonical - * shapes are the honest answer for a caller this template knows - * nothing about.

- * - * @return the canonical kit - */ @Override - public CvRenderKit kit() { - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java index 4c2a81191..3b8c70e2f 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/ModernProfessional.java @@ -2,15 +2,17 @@ import com.demcha.compose.document.api.DocumentSession; import com.demcha.compose.document.dsl.PageFlowBuilder; +import com.demcha.compose.document.dsl.SectionBuilder; import com.demcha.compose.document.style.DocumentColor; import com.demcha.compose.document.style.DocumentTextDecoration; import com.demcha.compose.document.style.DocumentTextStyle; import com.demcha.compose.document.templates.api.DocumentTemplate; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionDispatcher; import com.demcha.compose.document.templates.cv.data.CvDocument; import com.demcha.compose.document.templates.cv.data.CvSection; +import com.demcha.compose.document.templates.cv.data.ModuleSection; import com.demcha.compose.document.templates.cv.data.Slot; import com.demcha.compose.document.templates.core.theme.BrandTheme; import com.demcha.compose.document.templates.core.identity.ContactLine; @@ -124,10 +126,33 @@ public String displayName() { } @Override - public CvRenderKit kit() { - // This preset renders bodies through the shared dispatcher, so a - // runtime module already looks like the rest of its document. - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override @@ -187,7 +212,7 @@ public void compose(DocumentSession document, CvDocument doc) { SectionHeader.flat(host, sec.title(), SECTION_TITLE_COLOR, theme); }); pageFlow.addSection("Body_" + idx, host -> - SectionDispatcher.renderBody(host, sec, theme, kit())); + SectionDispatcher.renderBody(host, sec, theme, this)); } pageFlow.build(); diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java index 83de1908f..afac45609 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/MonogramSidebar.java @@ -336,23 +336,34 @@ public String displayName() { return DISPLAY_NAME; } - /** - * The canonical kit, and this template never consults it. - * - *

A kit is how a preset styles the bodies it routes through - * {@code SectionDispatcher}. This one routes none: a module is lowered - * by {@code SectionRouter} to the shape its slot draws, and the slot's - * own renderer draws it — which is why a runtime module already comes - * out in this preset's style rather than the canonical one. The kit is - * the value an outside caller would draw with, and the canonical - * shapes are the honest answer for a caller this template knows - * nothing about.

- * - * @return the canonical kit - */ @Override - public CvRenderKit kit() { - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java index 6fb1910cc..33f4b8c90 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/SidebarPortrait.java @@ -20,7 +20,7 @@ import com.demcha.compose.document.templates.core.text.MarkdownInline; import com.demcha.compose.document.templates.cv.components.ProjectLabel; import com.demcha.compose.document.templates.cv.api.ModularCvTemplate; -import com.demcha.compose.document.templates.cv.components.CvRenderKit; +import com.demcha.compose.document.templates.cv.components.ModuleRenderer; import com.demcha.compose.document.templates.cv.components.SectionAllocation; import com.demcha.compose.document.templates.cv.components.SectionRouter; import com.demcha.compose.document.templates.cv.components.SectionLookup; @@ -383,23 +383,34 @@ public String displayName() { return DISPLAY_NAME; } - /** - * The canonical kit, and this template never consults it. - * - *

A kit is how a preset styles the bodies it routes through - * {@code SectionDispatcher}. This one routes none: a module is lowered - * by {@code SectionRouter} to the shape its slot draws, and the slot's - * own renderer draws it — which is why a runtime module already comes - * out in this preset's style rather than the canonical one. The kit is - * the value an outside caller would draw with, and the canonical - * shapes are the honest answer for a caller this template knows - * nothing about.

- * - * @return the canonical kit - */ @Override - public CvRenderKit kit() { - return CvRenderKit.defaults(); + public void paragraph(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.paragraph(host, module, theme); + } + + @Override + public void bullets(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bullets(host, module, theme); + } + + @Override + public void bulletsStacked(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.bulletsStacked(host, module, theme); + } + + @Override + public void inlineList(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.inlineList(host, module, theme); + } + + @Override + public void entries(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entries(host, module, theme); + } + + @Override + public void entriesDated(SectionBuilder host, ModuleSection module, BrandTheme theme) { + ModuleRenderer.entriesDated(host, module, theme); } @Override diff --git a/templates/src/test/java/com/demcha/compose/document/templates/cv/api/CvConstructorKindGateTest.java b/templates/src/test/java/com/demcha/compose/document/templates/cv/api/CvConstructorKindGateTest.java new file mode 100644 index 000000000..9994bd123 --- /dev/null +++ b/templates/src/test/java/com/demcha/compose/document/templates/cv/api/CvConstructorKindGateTest.java @@ -0,0 +1,72 @@ +package com.demcha.compose.document.templates.cv.api; + +import com.demcha.compose.document.dsl.SectionBuilder; +import com.demcha.compose.document.templates.core.theme.BrandTheme; +import com.demcha.compose.document.templates.cv.data.CvKind; +import com.demcha.compose.document.templates.cv.data.ModuleSection; +import com.demcha.compose.document.templates.cv.presets.CvTemplates; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The constructor contract is the kinds: adding a {@link CvKind} adds a + * method, and every modular template has to declare it. A default on the + * interface would let a template absorb a new shape without noticing. + */ +class CvConstructorKindGateTest { + + @ParameterizedTest + @EnumSource(CvKind.class) + void everyKindHasANonDefaultConstructorMethod(CvKind kind) throws NoSuchMethodException { + Method method = CvConstructor.class.getMethod( + CvConstructor.methodName(kind), + SectionBuilder.class, ModuleSection.class, BrandTheme.class); + assertThat(method.isDefault()) + .as("%s must not have a default — a new kind has to break every template", + method.getName()) + .isFalse(); + assertThat(Modifier.isAbstract(method.getModifiers())) + .as("%s is the template's to implement", method.getName()) + .isTrue(); + } + + @Test + void theInterfaceHasNoSpareKindMethods() { + Set expected = Arrays.stream(CvKind.values()) + .map(CvConstructor::methodName) + .collect(Collectors.toSet()); + Set declared = Arrays.stream(CvConstructor.class.getDeclaredMethods()) + .filter(method -> !method.isDefault() && !method.isSynthetic()) + .filter(method -> method.getParameterCount() == 3) + .map(Method::getName) + .collect(Collectors.toSet()); + assertThat(declared).containsExactlyInAnyOrderElementsOf(expected); + } + + @Test + void everyModularTemplateDeclaresEveryKind() throws NoSuchMethodException { + assertThat(CvTemplates.modular()).isNotEmpty(); + for (ModularCvTemplate template : CvTemplates.modular()) { + Class type = template.getClass(); + for (CvKind kind : CvKind.values()) { + Method method = type.getMethod( + CvConstructor.methodName(kind), + SectionBuilder.class, ModuleSection.class, BrandTheme.class); + assertThat(method.getDeclaringClass()) + .as("%s must implement %s rather than inherit a default", + template.id(), kind) + .isNotEqualTo(CvConstructor.class) + .isNotEqualTo(ModularCvTemplate.class); + } + } + } +}