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
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ GraphCompose uses PDFBox under the hood as the rendering backend — the com
| You want to… | Surface | Entry point |
|---|---|---|
| Generate a one-off PDF programmatically | DSL | `GraphCompose.document(...).pageFlow(...)` — see [Hello world](#hello-world) below |
| Generate a CV / cover letter / invoice / proposal from data | Templates v2 | `ModernProfessional.create(BusinessTheme.modern()).compose(session, spec)` — see [templates v2](./docs/templates/v1-classic/README.md) |
| Generate a CV / cover letter from data | Layered templates | `ModernProfessional.create().compose(session, cvDocument)` — see [layered templates](./docs/templates/v2-layered/README.md) |
| Add a custom visual primitive | Engine extension | `NodeDefinition` + `PdfFragmentRenderHandler` — see [extension guide](./docs/contributing/extension-guide.md) |
| Regression-test generated layouts | Layout snapshots | `DocumentSession#layoutSnapshot()` — see [snapshot testing](./docs/operations/layout-snapshot-testing.md) |

Expand Down Expand Up @@ -146,7 +146,7 @@ For a Spring Boot `@RestController` streaming the PDF straight to the response,

## What's in v1.6 — "expressive"

- **Templates v2** — 14 CV and 14 paired cover-letter presets, theme-driven via `BusinessTheme`, one-liner `create(theme)` factories. Inline markdown, slot-based multi-column layouts. See [`docs/templates/v1-classic/README.md`](./docs/templates/v1-classic/README.md).
- **Layered templates** — 14 CV and 14 paired cover-letter presets on the layered `cv.v2` / `coverletter.v2` architecture (data → theme → components → widgets → presets), one-liner `create()` factories over a typed `CvDocument` / `CoverLetterDocument`. Inline markdown, multi-column layouts. The going-forward standard for new template families. See [`docs/templates/v2-layered/README.md`](./docs/templates/v2-layered/README.md). (The earlier `BusinessTheme`-based preset surface is now deprecated.)
- **Composed primitives** — `ListBuilder.addItem(label, Consumer)` (nested lists), `DocumentTableCell.node(...)` (any node inside a cell), `CanvasLayerNode` (pixel-precise free-canvas placement).
- **Architecture hardening** — `@Internal` API stability marker, public `PdfFragmentRenderHandler` SPI, `DocumentRenderingException` on the convenience render path, documented thread-safety contract.

Expand Down Expand Up @@ -193,7 +193,7 @@ document.pageFlow().addCanvas(523, 360, canvas -> canvas

### Templates
- 🆕 [**Templates — v2 layered architecture**](./docs/templates/v2-layered/README.md) — the canonical going-forward pattern for new template families (CV v2 is the reference implementation). Personas: [quickstart](./docs/templates/v2-layered/quickstart.md) · [using templates](./docs/templates/v2-layered/using-templates.md) · [authoring presets](./docs/templates/v2-layered/authoring-presets.md) · [contributing a new family](./docs/templates/v2-layered/contributor-guide.md).
- [Templates v1-classic landing](./docs/templates/v1-classic/README.md) — CV / cover-letter / invoice / proposal preset library (v1.6 surface, still shipped). Cheat sheet: [authoring](./docs/templates/v1-classic/authoring.md).
- [Templates v1-classic landing](./docs/templates/v1-classic/README.md) — the older `BusinessTheme` / `CvSpec` CV / cover-letter / invoice / proposal preset library (**deprecated** — CV + cover letter are superseded by [v2-layered](./docs/templates/v2-layered/README.md); invoice / proposal / schedule are not yet ported). Cheat sheet: [authoring](./docs/templates/v1-classic/authoring.md).

### Architecture & operations
- [Architecture overview](./docs/architecture/overview.md) · [Lifecycle](./docs/architecture/lifecycle.md) · [Production rendering](./docs/operations/production-rendering.md) · [Layout snapshot testing](./docs/operations/layout-snapshot-testing.md)
Expand Down
Binary file modified assets/readme/examples/cv-classic-serif.pdf
Binary file not shown.
Binary file modified assets/readme/examples/cv-compact-mono.pdf
Binary file not shown.
Binary file not shown.
Binary file modified assets/readme/examples/cv-modern-professional.pdf
Binary file not shown.
Binary file modified assets/readme/examples/cv-nordic-clean.pdf
Binary file not shown.
Binary file added assets/readme/examples/cv-panel.pdf
Binary file not shown.
Binary file removed assets/readme/examples/cv-product-leader.pdf
Binary file not shown.
Binary file removed assets/readme/examples/cv-tech-lead.pdf
Binary file not shown.
Binary file modified assets/readme/examples/cv-timeline-minimal.pdf
Binary file not shown.
3 changes: 2 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ it does.
- **[adr/0002-theme-unification.md](adr/0002-theme-unification.md)** — single canonical theme model.
- **[adr/0003-api-stability-and-internal-marker.md](adr/0003-api-stability-and-internal-marker.md)** — public-API guarantees + `@Internal` marker.
- **[adr/0004-pdf-handler-spi-extension.md](adr/0004-pdf-handler-spi-extension.md)** — PDF render handler SPI.
- **[adr/0011-templates-v2-architecture.md](adr/0011-templates-v2-architecture.md)** — the v1.6 templates restructure (spec/builder/presets/themes).
- **[adr/0011-templates-v2-architecture.md](adr/0011-templates-v2-architecture.md)** — the v1.6 templates restructure (spec/builder/presets/themes); **superseded** for CV + cover letter by [0015](adr/0015-layered-template-architecture.md).
- **[adr/0012-nested-list-evolution.md](adr/0012-nested-list-evolution.md)** — nested list rendering evolution.
- **[adr/0013-composed-table-cell.md](adr/0013-composed-table-cell.md)** — composed table cell model.
- **[adr/0014-controlled-absolute-placement.md](adr/0014-controlled-absolute-placement.md)** — controlled absolute placement strategy.
- **[adr/0015-layered-template-architecture.md](adr/0015-layered-template-architecture.md)** — the layered `cv.v2` / `coverletter.v2` authoring model (current standard); supersedes the preset/builder portion of 0011.

> **ADR numbering gap (0005–0010)** is intentional — those numbers
> were reserved during a v1.5 restructure that landed under ADR 0011
Expand Down
80 changes: 80 additions & 0 deletions docs/adr/0015-layered-template-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# ADR 0015 — Layered template architecture (cv.v2 / coverletter.v2)

- **Status:** Accepted
- **Date:** 2026-05-28
- **Authors:** Artem Demchyshyn

## Context

ADR 0011 reorganised templates into a per-domain "Templates v2" surface
(`templates/cv/{presets,builder,spec,layouts}`, `templates/coverletter/…`)
built on `CvSpec` / `CoverLetterSpec` data records, `BusinessTheme`, and
flat copy-and-tweak preset classes. That removed the v1.5 mess (one
600–700-line composer per template), but two limits remained:

- **Preset classes still re-implemented header / contact / section
rendering.** Each preset carried its own `addHeader`, contact row, and
body dispatch — the duplication ADR 0011 set out to kill kept creeping
back at the preset layer.
- **Style, layout, and data were not cleanly separated.** A visual
re-skin still meant editing preset code; there was no single token
source a preset read from.

A refined **layered** architecture was prototyped under `cv/v2`, proved
out across all 14 CV presets (with a pixel-parity migration from the
Gen-2 presets), then extended to all 14 cover letters under
`coverletter/v2` (which reuses the CV theme + components so a CV and its
paired letter render as a matched set).

## Decision

Adopt the **layered** template architecture as the canonical
template-authoring pattern. A template family is built in five layers:

1. **data** — typed input records (`CvDocument`, `CoverLetterDocument`,
reusing `CvIdentity`); no rendering logic.
2. **theme** — `CvTheme` = palette + typography + spacing + decoration
tokens; the only place colour / font / spacing literals live.
3. **components** — shared stateless renderers (`SectionDispatcher`,
`RichParagraphRenderer`, `MarkdownInline`, `CvTextStyles`,
`LetterBody`, …) reused across presets and families.
4. **widgets** — composable visual blocks (`Headline`, `ContactLine`,
`Masthead`, `CardWidget`, `SectionHeader`, …).
5. **presets** — thin orchestrators: a `create()` / `create(CvTheme)`
factory plus a `compose()` that lays out the page-flow and delegates
to components / widgets. No re-implemented parsing, no duplicated
headers.

Presets are exposed through the generic `DocumentTemplate<T>` contract.
`cv.v2` is the reference implementation; `coverletter.v2` is the paired
family.

The earlier Gen-2 surface
(`templates/cv/{presets,builder,spec,layouts}` and the equivalent
`templates/coverletter/{presets,builder,spec,layouts}`) is **deprecated**
(`@Deprecated(since = "1.7.0", forRemoval = true)`) and scheduled for
removal in a future major. It keeps compiling and working until then —
existing callers are not broken.

## Consequences

- **One documented authoring path.** New template families follow
`docs/templates/v2-layered/` (quickstart · using-templates ·
authoring-presets · contributor-guide). A new preset is a thin
orchestrator, not a forked composer.
- **Re-skin without code edits.** A new visual flavour is a new
`CvTheme.<brand>()` factory; the preset is unchanged.
- **Naming overlap resolved.** Gen-2 package-info prose previously also
called itself "Templates v2", colliding with the `cv.v2` folder name.
Deprecating Gen-2 and correcting its package-info removes the
ambiguity; "the v2 / layered surface" now unambiguously means
`cv.v2` / `coverletter.v2`.
- **Showcase + examples render the layered surface.** The CV and
cover-letter gallery examples and the committed README previews are
generated from `cv.v2` / `coverletter.v2`.
- **Not yet ported:** invoice, proposal, and schedule remain on the
Gen-2 / builtins surface (`BusinessTheme`-based) and are **not**
deprecated. Porting them to the layered architecture is future work.
- **Supersedes the preset / builder / spec portion of ADR 0011** while
keeping its domain-folder split (cv / coverletter / invoice /
proposal / schedule).
10 changes: 5 additions & 5 deletions docs/roadmaps/migration-v1-5-to-v1-6.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,13 +332,13 @@ try (DocumentSession session = GraphCompose.document(out).create()) {
```

The full builder fluent surface and module / block taxonomy are
documented in [`docs/templates/v1-classic/authoring.md`](template-authoring.md).
The full gallery of v2 CV / cover-letter renders lives under
[`assets/readme/examples/`](../assets/readme/examples/) and is
documented in [`docs/templates/v1-classic/authoring.md`](../templates/v1-classic/authoring.md).
The full gallery of CV / cover-letter renders lives under
[`assets/readme/examples/`](../../assets/readme/examples/) and is
regenerable via
[`examples/CvTemplateGalleryFileExample`](../examples/src/main/java/com/demcha/examples/CvTemplateGalleryFileExample.java)
[`CvTemplateGalleryFileExample`](../../examples/src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java)
and
[`CoverLetterTemplateGalleryFileExample`](../examples/src/main/java/com/demcha/examples/CoverLetterTemplateGalleryFileExample.java).
[`CoverLetterTemplateGalleryFileExample`](../../examples/src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java).

### What v2 gives you for free

Expand Down
14 changes: 7 additions & 7 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,8 +214,8 @@ Generates every v2 CV preset in one orchestrated run — 14 presets
covering single-column, two-column-sidebar, and three-column-magazine
layouts. Use this as the side-by-side catalogue when picking a base
preset for your own CV product. Each preset is a one-liner factory
(`ModernProfessional.create(theme)`, `NordicClean.create(theme)`,
…); see `templates/cv/presets/` for the full list.
(`ModernProfessional.create()`, `NordicClean.create()`,
…); see `cv/v2/presets/` for the full list.

| Variant | PDF |
|---|---|
Expand All @@ -224,8 +224,8 @@ preset for your own CV product. Each preset is a one-liner factory
| Classic serif | [PDF](../assets/readme/examples/cv-classic-serif.pdf) |
| Compact mono | [PDF](../assets/readme/examples/cv-compact-mono.pdf) |
| Timeline minimal | [PDF](../assets/readme/examples/cv-timeline-minimal.pdf) |
| Engineering resume (was "Tech lead") | [PDF](../assets/readme/examples/cv-tech-lead.pdf) |
| Panel (was "Product leader") | [PDF](../assets/readme/examples/cv-product-leader.pdf) |
| Engineering resume | [PDF](../assets/readme/examples/cv-engineering-resume.pdf) |
| Panel | [PDF](../assets/readme/examples/cv-panel.pdf) |
| Executive · BoxedSections · CenteredHeadline · BlueBanner · EditorialBlue · SidebarPortrait · MonogramSidebar | run the gallery to render |

[📜 Full source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java)
Expand All @@ -235,9 +235,9 @@ preset for your own CV product. Each preset is a one-liner factory
Generates all 14 paired v2 cover-letter presets in one run — one
letter style per CV preset so a candidate's CV and cover letter
share the same visual language end-to-end. Each preset is a
one-liner factory (`ModernProfessionalLetter.create(theme)`,
`NordicCleanLetter.create(theme)`, …) under
`templates/coverletter/presets/`.
one-liner factory (`ModernProfessionalLetter.create()`,
`NordicCleanLetter.create()`, …) under
`coverletter/v2/presets/`.

[📜 Full source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.demcha.compose.document.templates.blocks.WorkHistoryBlock;
import com.demcha.compose.document.templates.coverletter.spec.CoverLetterHeader;
import com.demcha.compose.document.templates.coverletter.spec.CoverLetterSpec;
import com.demcha.compose.document.templates.coverletter.v2.data.CoverLetterDocument;
import com.demcha.compose.document.templates.cv.spec.CvHeader;
import com.demcha.compose.document.templates.cv.spec.CvModule;
import com.demcha.compose.document.templates.cv.spec.CvSpec;
Expand Down Expand Up @@ -451,6 +452,53 @@ public static CoverLetterHeader sampleCoverLetterHeaderV2() {

// -- Templates v2 (cv/v2) sample data ------------------------------

/**
* Returns the canonical Jordan Rivera identity shared by the v2 CV
* sample and the v2 cover-letter sample, so a CV and its paired
* letter render an identical masthead.
*
* @return sample v2 identity block
*/
public static CvIdentity sampleCvIdentityV2() {
return CvIdentity.builder()
.name("Jordan", "Rivera")
.jobTitle("Platform Engineer")
.contact("+44 20 5555 1000",
"jordan.rivera@example.com",
"London, UK")
.link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo")
.link("GitHub", "https://github.com/jrivera-demo")
.build();
}

/**
* Returns a sample {@code CoverLetterDocument} for the v2
* cover-letter pipeline. Reuses {@link #sampleCvIdentityV2()} so the
* letter masthead matches the paired CV exactly; the greeting / body
* / closing reuse the canonical v2 cover-letter sample content.
*
* @return sample v2 cover letter document
*/
public static CoverLetterDocument sampleCoverLetterDocumentV2() {
return CoverLetterDocument.builder()
.identity(sampleCvIdentityV2())
.greeting("Dear Hiring Team at **Northwind Systems**,")
.paragraph("I am excited to share my interest in the Senior "
+ "Platform Engineer role. My recent work has focused "
+ "on building **reusable document-generation systems** "
+ "that balance public API design, render quality, and "
+ "maintainability.")
.paragraph("I enjoy translating fuzzy workflow requirements into "
+ "clear template abstractions, reliable test coverage, "
+ "and examples that make adoption easier for the rest "
+ "of the team.")
.paragraph("I would welcome the opportunity to bring that same "
+ "mix of engineering rigor and product thinking to your "
+ "platform group.")
.closing("Sincerely,")
.build();
}

/**
* Returns a sample {@code CvDocument} for the v2 CV pipeline —
* the canonical Jordan Rivera content expressed in the v2
Expand All @@ -470,15 +518,7 @@ public static CoverLetterHeader sampleCoverLetterHeaderV2() {
* @return sample v2 CV document
*/
public static CvDocument sampleCvDocumentV2() {
CvIdentity identity = CvIdentity.builder()
.name("Jordan", "Rivera")
.jobTitle("Platform Engineer")
.contact("+44 20 5555 1000",
"jordan.rivera@example.com",
"London, UK")
.link("LinkedIn", "https://linkedin.com/in/jordan-rivera-demo")
.link("GitHub", "https://github.com/jrivera-demo")
.build();
CvIdentity identity = sampleCvIdentityV2();

ParagraphSection summary = new ParagraphSection(
"Professional Summary",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.demcha.examples.support;

import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.rendering.PDFRenderer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.nio.file.Path;

/**
* Tiny dev/review utility: rasterizes each page of a PDF to a PNG via
* PDFBox so generated documents can be eyeballed without a system
* Ghostscript/poppler install.
*
* <pre>{@code
* exec:java -Dexec.mainClass=com.demcha.examples.support.PdfRasterizer \
* -Dexec.args="path/to/doc.pdf out/prefix 140"
* }</pre>
*
* <p>Writes {@code prefix-p0.png}, {@code prefix-p1.png}, … one per
* page.</p>
*/
public final class PdfRasterizer {

private PdfRasterizer() {
}

public static void main(String[] args) throws Exception {
if (args.length < 2) {
System.err.println("usage: PdfRasterizer <pdf> <outPrefix> [dpi]");
System.exit(2);
}
Path pdf = Path.of(args[0]);
String prefix = args[1];
float dpi = args.length > 2 ? Float.parseFloat(args[2]) : 140f;

try (PDDocument doc = Loader.loadPDF(pdf.toFile())) {
PDFRenderer renderer = new PDFRenderer(doc);
int pages = doc.getNumberOfPages();
for (int i = 0; i < pages; i++) {
BufferedImage image = renderer.renderImageWithDPI(i, dpi);
File out = new File(prefix + "-p" + i + ".png");
ImageIO.write(image, "png", out);
System.out.println("Wrote: " + out.getAbsolutePath());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ record Entry(String title, String description, List<String> tags, String codeUrl
cv("cv-panel", "Panel", "Soft-tinted panels per section, Product-Leader feel — was ProductLeader in v1.5.", "panel");
cv("cv-sidebar-portrait", "Sidebar Portrait", "Edge-to-edge grey sidebar with portrait photo, contact stack, and skills.", "sidebar", "portrait");
cv("cv-monogram-sidebar", "Monogram Sidebar", "Sidebar with monogram badge, accent rule, and structured contact + skills column.", "sidebar", "monogram");
cv("cv-modern-professional", "Modern Professional CV", "Same data, ModernProfessional preset — single-file CvFileExample for canonical authoring.", "minimal");

// ===== Templates / Cover Letter =====
letter("cover-letter", "Cover Letter (canonical)", "Single-file canonical cover letter authored via CoverLetterFileExample.", "letter");
Expand Down
Loading