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
105 changes: 104 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,110 @@
All notable changes to GraphCompose are documented here. Versions
follow semantic versioning; release dates are ISO 8601.

## v2.2.1 — Planned
## v2.3.0 — Planned

### Public API

- **A CV section whose shape is a value, for CVs assembled at runtime.** The four
section records each fix one shape at compile time, which is right when a CV is
written in Java — you pick the record, the compiler checks it. It is the wrong model
when the CV arrives as data: a user who has just chosen "Volunteering, shaped like
Education, with dates" cannot instantiate a different record per choice, so every
shape somebody thought of would have to become a type.

`ModuleSection` carries the choice instead. One `CvItem` record holds every optional
field — title, link, subtitle, period, location, description lines — and a `CvKind`
(`PARAGRAPH`, `BULLETS`, `BULLETS_STACKED`, `INLINE_LIST`, `ENTRIES`,
`ENTRIES_DATED`) decides which of them are read: the same item renders with or without its dates depending on the kind
alone. `BodyStyle` decides whether a description reads as prose or as bullets, and
`SectionRole` states what a section *means* — the decision multi-column presets make
by matching headings against English keywords, which a CV headed `Ausbildung` or
`Навыки` never matches. The presets do not read the role yet; it travels with the
section now so a document built today needs no rewrite when the routing work lands.

The existing four records are untouched and mix with modules in the same document.
A module renders through the existing components rather than beside them, so one
drawn as `ENTRIES_DATED` lays out exactly like the `EntriesSection` carrying the same
content — held node-for-node by a parity suite, for every kind, alongside the
extracted text so structure and content are both pinned. The addition is binary-
compatible (the japicmp gate covers this module); it is a fifth permit on a sealed
interface, so a downstream `switch` over `CvSection` that was exhaustive without a
`default` needs one.

- **Which presets can be handed a runtime module, declared rather than assumed.** A module
is only useful if the template renders it, and not every preset can promise that:
several compose a fixed set of modules and find each by matching headings, so a
section they do not recognise never reaches a renderer. The CV still comes out —
minus a section, looking finished — which is the kind of failure nobody reports.

`ModularCvTemplate` is the promise, and `CvTemplates.modular()` is the list a CV
builder should offer; `CvTemplates` also answers `byId`, `all`, `ids`, and
`recommendedMargin`, so picking a preset at runtime stops being a hand-kept map in
every consumer. Declaring the interface is not free: `ModularCvTemplateFidelityTest`
renders a document carrying every kind, an invented heading, a heading in a
script no keyword list contains, and a heading that *does* match one, through each
template that declares it, and asserts every item reached the page under the words
the author wrote — the last case because `EditorialBlue` renamed any heading
matching "certification" to EDUCATION, so "Certifications & Awards" arrived as a
word nobody had written. The promise covers `Slot.MAIN`, and says so: every shipped
preset composes a single main column, so a sidebar section is dropped by these
templates as by every other. Seven presets qualify today. `ClassicSerif` does not,
and finding that out is what the gate is for — it draws any shape it is given, but
only gives itself the sections it recognises.

`CvTemplatesCoverageTest` derives the catalogue from the presets package rather than
trusting it, so a preset that ships without being registered fails the build instead
of being invisible to every caller that looks a template up by id.

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

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

Nine presets and every slot they compose changed; a CV written in Russian and German
now renders on all sixteen, which `RoleRoutingTest` holds by rendering one.

- **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.

### Fixed

- **A section shape a preset did not recognise was lost three different ways.**
`BlueBanner` and `ClassicSerif` each kept a private copy of the section dispatcher
whose final `else` threw `IllegalStateException`; `EditorialBlue`'s had no `else` at
all; and `SectionLookup.hasContent` — which presets consult *before* routing, and
which `SectionAllocation.remaining()` uses to decide what still needs a home —
answered `false` for any subtype it had not been taught, dropping the heading along
with the body. So a section type added to the model would have crashed two presets
and vanished from several more, including through the very fallback that exists to
catch unplaced sections. All three dispatchers now delegate unfamiliar shapes to the
canonical one, and `hasContent` answers for every permit.

- **An entry with no date no longer reserves a column for it.** `EntryRenderer` always
emitted the two-column title/date header, so an undated entry — a certification, a
project — had its title wrapped early to leave room for nothing. Its Javadoc had
described the collapsing behaviour since the entry renderer was written. No shipped
fixture has a blank date, so no existing render moves.

### Build

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@

> **Release status** —
> 🟢 **Latest stable**: [v2.2.0](https://github.com/DemchaAV/GraphCompose/releases/tag/v2.2.0) — the **right-to-left** release: Hebrew and Arabic lay out, shape, join and mirror through PDF, PowerPoint and Word — in paragraphs and in table cells — with the fonts to render them. See [CHANGELOG.md](./CHANGELOG.md).
>  ·  🟡 **In development**: v2.2.1 on `develop` — see [CHANGELOG.md](./CHANGELOG.md).
>  ·  🟡 **In development**: v2.3.0 on `develop` — see [CHANGELOG.md](./CHANGELOG.md).

<p align="center">
<a href="https://demchaav.github.io/GraphCompose/"><b>Live Showcase</b></a>
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-build</artifactId>
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion bundle/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
graph-compose and graph-compose-templates dependencies below use
${project.version}, so they follow automatically.
-->
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>GraphCompose Bundle</name>
Expand Down
2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-core</artifactId>
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>

<name>GraphCompose Core</name>
<description>A declarative layout engine for programmatic document generation, implemented primarily in Java. This is the lean engine coordinate; depend on the `graph-compose` artifact for the drop-in, PDF-capable install.</description>
Expand Down
2 changes: 1 addition & 1 deletion coverage/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
<parent>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-build</artifactId>
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion docs/roadmaps/post-2.0-engineering.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ exercise it. Report-only; thresholds can follow after a baseline read.
`japicmp` ran report-only through the 2.0 major — the major intentionally broke
binary compatibility. With the 2.0.0 GA artifacts on Central the gate switched to
per-module baselines pinned at the major's floor (2.0.0) in break-on-incompatible
mode: `graph-compose-core` first, `graph-compose-templates` since 2.2.1 — each
mode: `graph-compose-core` first, `graph-compose-templates` since 2.3.0 — each
module's `japicmp` profile lives in its own pom and runs in the PR-time
`Binary Compatibility` job, in `cut-release.ps1` step 5b, and in the publish
workflow. The remaining published modules (`render-pdf` / `render-docx` /
Expand Down
115 changes: 110 additions & 5 deletions docs/templates/v2-layered/using-templates.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ it sets up the conceptual model in 5 minutes.
1. [The pieces you assemble](#the-pieces-you-assemble)
2. [Identity — name, contact, optional links](#identity)
3. [Section types](#section-types)
4. [Slots — main vs sidebar](#slots)
5. [Picking a preset](#picking-a-preset)
6. [Customising a theme](#customising-a-theme)
7. [Rendering — pageSize, margins, output](#rendering)
8. [Common patterns](#common-patterns)
4. [Building sections at runtime — `ModuleSection`](#runtime-modules)
5. [Slots — main vs sidebar](#slots)
6. [Picking a preset](#picking-a-preset)
7. [Customising a theme](#customising-a-theme)
8. [Rendering — pageSize, margins, output](#rendering)
9. [Common patterns](#common-patterns)

---

Expand Down Expand Up @@ -161,6 +162,110 @@ paragraph beneath.

---

<a id="runtime-modules"></a>
## Building sections at runtime — `ModuleSection`

The four types above are the right choice when you write a CV in Java:
you pick the record and the compiler checks it. They are the wrong one
when the CV is assembled from data — a form, a JSON payload, an LLM —
because the shape is not known until it arrives, and a user who picks
"dated entries" from a menu cannot instantiate a different record per
choice.

`ModuleSection` moves that choice into a value. One item record carries
every optional field, and a `CvKind` decides which of them are read:

```java
ModuleSection.builder("Volunteering", SectionRole.OTHER, CvKind.ENTRIES_DATED)
.item(CvItem.of("Mentor, Rails Girls")
.at("Rails Girls Berlin") // subtitle
.in("Berlin, DE") // location
.period("2019 - 2021") // read by dated kinds only
.bullets("Ran three weekend workshops"))
.build();
```

| `CvKind` | Shape | Reads |
|---|---|---|
| `PARAGRAPH` | prose under the section heading | `body` |
| `BULLETS` | a bullet per item, description inline | `title`, `body` (not `link`) |
| `BULLETS_STACKED` | a bullet per item, description underneath | `title`, `link`, `body` |
| `INLINE_LIST` | `Languages: Java 21, Kotlin` | `title`, `body` (not `link`) |
| `ENTRIES` | timeline, no date column | everything but `period` |
| `ENTRIES_DATED` | timeline with dates | everything |

Only `title` is required on an item. Whatever a kind does not read is
ignored, so the same item renders with or without its dates depending
on the kind alone — which is what lets a "Volunteering" module be
shaped exactly like Education without a new type.

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

A heading that matches a keyword still routes a section that has no
role — every hand-written section, and any module you left as
`SectionRole.OTHER`. What a heading may not do is overrule a role: a
module declared `EXPERIENCE` and headed "Projects" goes where you put
it, and the projects slot does not also claim it.

Modules and the four fixed types mix freely in one document, and both
render through the same components — a module drawn as `ENTRIES_DATED`
lays out exactly like the `EntriesSection` carrying the same content.

### Which preset can you hand a runtime module to?

Not every preset. Several compose a fixed set of modules and find each by
matching headings, so a section they do not recognise never reaches a
renderer; the CV still comes out, minus a section, looking finished. The
ones that render whatever they are handed say so in the type system:

```java
List<ModularCvTemplate> safe = CvTemplates.modular(); // offer these

CvTemplates.byId("modern-professional") // or look one up
.orElseThrow()
.compose(session, doc);
```

`CvTemplates` also answers `all()`, `ids()`, and `recommendedMargin(id)` —
the margin a preset was designed at, which you need while building the
session, before you have a template.

Declaring `ModularCvTemplate` is not free: a fidelity suite renders a
document carrying every kind, an invented heading, a heading in a script no
keyword list contains, and a heading that *does* match one, through each
template that declares it, and asserts every item reached the page under the
author's own words.

The promise covers `Slot.MAIN`, which is where sections go unless you say
otherwise. Every shipped preset composes a single main column, so a section
placed in `Slot.SIDEBAR` is dropped — by these templates as by every other.

The presets outside that list are not broken, they are *designed*: each
composes a fixed set of slots, so it renders the roles it has a place for
and drops a section it has no slot for. Give such a preset a CV whose
sections map onto roles and it renders them all; give it an extra
"Volunteering" module and that one is lost. The reason is structural — the
whole body of those presets is one atomic block that cannot break across
pages, so there is nowhere to put an extra section — and lifting it needs
pagination work, not routing.

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()`.

---

<a id="slots"></a>
## Slots — main vs sidebar

Expand Down
2 changes: 1 addition & 1 deletion examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-build</artifactId>
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-build</artifactId>
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<packaging>pom</packaging>

<name>GraphCompose Build Aggregator</name>
Expand Down
2 changes: 1 addition & 1 deletion qa/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<parent>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-build</artifactId>
<version>2.2.1-SNAPSHOT</version>
<version>2.3.0-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
Loading
Loading