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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,60 @@ This guide answers the **"how do I…"** questions.

---

## The scaffold is persona-neutral

Nothing in the v2 API assumes a software-developer audience. The
sample data shipped with the package happens to be a developer CV
(Jordan Rivera, "Platform Engineer", GitHub, Projects, Tech Skills),
but that's only because we needed *some* fixture for visual
regression. The same builders fit any persona — skip the link / section
types you don't need.

```java
// A primary school teacher's CV — no GitHub, no Projects, no Tech Skills
CvDocument.builder()
.identity(CvIdentity.builder()
.name("Maria", "Lopez")
.contact("+34 600 000 000", "maria@example.com", "Madrid, Spain")
// no .link(...) calls — she has no public profiles, and that is fine
.build())
.section(new ParagraphSection("About Me",
"Primary school teacher with 12 years' experience in literacy " +
"and inclusive education."))
.section(EntriesSection.builder("Teaching Experience")
.entry("Lead Teacher Y3", "Colegio Santa Ana", "2018-Present",
"Year-3 lead teacher; designed the school's reading-rota " +
"and mentored two newly-qualified teachers.")
.entry("Year Teacher", "Escuela Primaria Goya", "2013-2018",
"Y1-Y2 generalist; led the SEN reading-club after hours.")
.build())
.section(RowsSection.builder("Languages", RowStyle.PLAIN)
.row("Spanish", "Native")
.row("English", "Fluent (CEFR C1)")
.row("Catalan", "Conversational")
.build())
.section(RowsSection.builder("Certifications", RowStyle.PLAIN)
.row("Inclusive Education", "Universidad Complutense, 2020")
.row("Children's First Aid", "Cruz Roja, 2022")
.build())
.build();
```

Notice what's absent:

- **No `link(...)` calls.** Optional, simply omitted.
- **No Projects, no Skills.** The three section types
(`ParagraphSection`, `RowsSection`, `EntriesSection`) work for any
content shape — you choose what to put in them and what to call
them.
- **No required IT vocabulary anywhere.** Section titles are free
strings (`"About Me"`, `"Teaching Experience"`, `"Certifications"`).

The rest of this guide leans on dev-style examples for continuity,
but every recipe below works the same way for any persona.

---

## Recipe 1 — change a bullet glyph

You want `▶` instead of `•`, or numbered bullets, or em-dashes.
Expand Down Expand Up @@ -211,7 +265,52 @@ guard.)

---

## Recipe 6 — conditional sections (data-driven)
## Recipe 6 — place sections in slots (sidebar / footer)

A `CvDocument` is not just a flat list of sections — every section is
**placed** into a `Slot` (one of `MAIN`, `SIDEBAR`, `FOOTER`).
Single-column presets like `BoxedSections` read only `Slot.MAIN`
sections; multi-column presets read whichever slots they support.

**Placing a section into a slot** (builder API):

```java
CvDocument doc = CvDocument.builder()
.identity(identity)
.section(summary) // defaults to MAIN
.section(Slot.MAIN, technicalSkills) // explicit, same as above
.section(Slot.SIDEBAR, languagesSpoken) // sidebar column
.section(Slot.SIDEBAR, certifications) // sidebar column
.sections(Slot.MAIN, experience, projects) // varargs in a slot
.build();
```

**Reading by slot** (in a preset's `compose()`):

```java
// Single-column preset — render only MAIN, drop the rest
List<CvSection> mainSections = doc.sectionsIn(Slot.MAIN);

// Two-column preset
List<CvSection> mainSections = doc.sectionsIn(Slot.MAIN);
List<CvSection> sidebarSections = doc.sectionsIn(Slot.SIDEBAR);
```

**Why this matters:** when the user feeds your document to a
single-column preset, sidebar sections are silently dropped (they
have no place to render). When the same document goes through a
two-column preset, sidebar sections show up in the sidebar column.
The data model is the same — only the preset's interpretation
differs.

**Tip:** `doc.sections()` returns *every* section in source order
regardless of slot. Use it for debug printing or unfiltered
iteration — not in a preset's render loop unless you really want
sidebar content to flow inline with main.

---

## Recipe 7 — conditional sections (data-driven)

Real CVs often hide a section when there's nothing to put in it:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,127 @@

/**
* Root of the v2 CV data model — required {@link CvIdentity} block
* plus an ordered list of {@link CvSection} entries that render in
* source order beneath the header.
* plus an ordered list of section {@link Placement}s.
*
* <p>This record carries no styling or rendering decision. Pair it
* with a
* {@link com.demcha.compose.document.templates.cv.v2.theme.CvTheme}
* and a preset from
* {@code com.demcha.compose.document.templates.cv.v2.presets} to
* produce a PDF.</p>
* <p>Each {@link Placement} pairs a {@link CvSection} with a
* {@link Slot} so multi-column presets can decide which sections go
* into the sidebar vs. the main column without having to guess from
* section titles. Sections built without an explicit slot default to
* {@link Slot#MAIN}.</p>
*
* @param identity required identity / contact block
* @param sections ordered sections; rendered in source order
* <p>Single-column presets read {@link #sectionsIn(Slot)
* sectionsIn(Slot.MAIN)} and ignore the rest; multi-column presets
* call {@code sectionsIn} once per slot they support.</p>
*
* @param identity required identity / contact block
* @param placements ordered placements; render order matches source
* order within each slot
*/
public record CvDocument(CvIdentity identity, List<CvSection> sections) {
public record CvDocument(CvIdentity identity, List<Placement> placements) {

public CvDocument {
Objects.requireNonNull(identity, "identity");
Objects.requireNonNull(sections, "sections");
sections = List.copyOf(sections);
Objects.requireNonNull(placements, "placements");
placements = List.copyOf(placements);
}

/**
* Backward-compatible factory that wraps every supplied section
* in a {@link Slot#MAIN} placement. Provided so callers
* migrating from the pre-slot {@code new CvDocument(identity,
* sections)} pattern have a 1:1 replacement without rewriting
* their construction code.
*
* <p>Equivalent to building each section through
* {@link Builder#section(CvSection)} (which also defaults to
* {@link Slot#MAIN}). Prefer the {@link Builder} in new code —
* multi-column presets need explicit slot placement that the
* builder makes obvious at the call site.</p>
*
* @deprecated since the slot model — prefer the {@link Builder}
* so non-MAIN slots are visible at the call site.
*/
@Deprecated
public static CvDocument ofMainSections(CvIdentity identity,
List<CvSection> sections) {
return new CvDocument(identity, toMainPlacements(sections));
}

/**
* One section together with the slot it should render in.
*
* @param slot placement region (never null)
* @param section section body (never null)
*/
public record Placement(Slot slot, CvSection section) {

public Placement {
Objects.requireNonNull(slot, "slot");
Objects.requireNonNull(section, "section");
}
}

// -- accessors -------------------------------------------------------

/**
* Returns every section in this document in source order,
* regardless of slot. Useful for debugging, indexing, or
* iterating all sections without caring where they render.
*
* <p>Presets should usually call {@link #sectionsIn(Slot)}
* instead — a single-column preset that iterates this flat list
* will accidentally render sidebar content inline with the main
* flow.</p>
*
* @return flat list of all sections, source order
*/
public List<CvSection> sections() {
List<CvSection> out = new ArrayList<>(placements.size());
for (Placement p : placements) {
out.add(p.section());
}
return List.copyOf(out);
}

/**
* Returns the sections placed in {@code slot}, in source order.
* The standard call from a preset's {@code compose()} method.
*
* @param slot non-null slot
* @return sections placed in that slot; empty list if none
*/
public List<CvSection> sectionsIn(Slot slot) {
Objects.requireNonNull(slot, "slot");
List<CvSection> out = new ArrayList<>();
for (Placement p : placements) {
if (p.slot() == slot) {
out.add(p.section());
}
}
return List.copyOf(out);
}

/**
* Returns the slot a given section was placed in. Uses identity
* comparison ({@code ==}) to avoid surprises when two sections
* happen to be record-equal but were added separately.
*
* @param section section instance to look up
* @return its slot, or {@link Slot#MAIN} if the section is not
* in this document
*/
public Slot slotOf(CvSection section) {
Objects.requireNonNull(section, "section");
for (Placement p : placements) {
if (p.section() == section) {
return p.slot();
}
}
return Slot.MAIN;
}

// -- builder ---------------------------------------------------------

/**
* @return new fluent builder
*/
Expand All @@ -35,12 +135,16 @@ public static Builder builder() {
}

/**
* Mutable builder. Section order in the builder is preserved 1:1
* in the built document.
* Mutable builder for {@link CvDocument}.
*
* <p>The {@code section}-family methods append to the placement
* list in call order. Pass a {@link Slot} as the first argument
* to place into a non-main slot; omit it to default to
* {@link Slot#MAIN}.</p>
*/
public static final class Builder {
private CvIdentity identity;
private final List<CvSection> sections = new ArrayList<>();
private final List<Placement> placements = new ArrayList<>();

private Builder() {
}
Expand All @@ -50,37 +154,76 @@ public Builder identity(CvIdentity value) {
return this;
}

/**
* Appends one section into {@link Slot#MAIN}.
*/
public Builder section(CvSection section) {
this.sections.add(Objects.requireNonNull(section, "section"));
return section(Slot.MAIN, section);
}

/**
* Appends one section into a chosen slot.
*/
public Builder section(Slot slot, CvSection section) {
this.placements.add(new Placement(slot, section));
return this;
}

/**
* Varargs convenience for the common case where every section
* is constructed up-front. Equivalent to chained
* {@link #section(CvSection)} calls.
*
* @param values one or more sections in render order
* @return this builder
* Varargs convenience — all sections placed in
* {@link Slot#MAIN}.
*/
public Builder sections(CvSection... values) {
Objects.requireNonNull(values, "values");
for (CvSection s : values) {
section(s);
section(Slot.MAIN, s);
}
return this;
}

/**
* Varargs convenience — all supplied sections placed in
* {@code slot}.
*/
public Builder sections(Slot slot, CvSection... values) {
Objects.requireNonNull(slot, "slot");
Objects.requireNonNull(values, "values");
for (CvSection s : values) {
section(slot, s);
}
return this;
}

/**
* List variant — all sections placed in {@link Slot#MAIN}.
*/
public Builder sections(List<CvSection> values) {
Objects.requireNonNull(values, "values");
for (CvSection s : values) {
section(s);
section(Slot.MAIN, s);
}
return this;
}

/**
* Appends a pre-built {@link Placement}.
*/
public Builder placement(Placement placement) {
this.placements.add(Objects.requireNonNull(placement, "placement"));
return this;
}

public CvDocument build() {
return new CvDocument(identity, sections);
return new CvDocument(identity, placements);
}
}

private static List<Placement> toMainPlacements(List<CvSection> sections) {
Objects.requireNonNull(sections, "sections");
List<Placement> out = new ArrayList<>(sections.size());
for (CvSection s : sections) {
out.add(new Placement(Slot.MAIN, s));
}
return out;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.demcha.compose.document.templates.cv.v2.data;

/**
* Logical placement region inside a {@link CvDocument}. Presets read
* the slot of each section to decide where on the page it should
* appear — main column, sidebar, or footer.
*
* <p>Single-column presets like
* {@code com.demcha.compose.document.templates.cv.v2.presets.BoxedSections}
* iterate only {@link #MAIN} sections; sections placed in
* {@link #SIDEBAR} or {@link #FOOTER} are silently dropped by such
* presets. Multi-column presets (two-column sidebar, magazine
* layouts) iterate {@link #MAIN} and {@link #SIDEBAR} separately to
* fill their columns.</p>
*
* <p>Sections that don't specify a slot at build time default to
* {@link #MAIN} — so single-column callers keep working without
* changes after the slot model was introduced.</p>
*/
public enum Slot {

/** Primary content column. Default when no slot is specified. */
MAIN,

/** Sidebar / secondary column — typically narrower than MAIN. */
SIDEBAR,

/**
* Footer area below the main flow — useful for references,
* disclaimers, or page-bottom notes. Currently only rendered by
* presets that explicitly opt in.
*/
FOOTER
}
Loading