From bedbecb7a63343c03c3e08d21e72ab37963dc564 Mon Sep 17 00:00:00 2001
From: DemchaAV 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. Kept next to the methods themselves so a test can prove the
+ * bijection without copying the names. 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.
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) { + ListTyped 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