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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ follow semantic versioning; release dates are ISO 8601.

### Public API

- **`CvEntry` carries a link.** An entry that points somewhere — a repository, a case
study, a company — had no way to say so, and a preset had no way to make its title
reachable. `CvEntry` now carries `link`, a plain string blank when absent like the
fields beside it, set through `CvEntry.Builder.link(...)`. It costs the layout
nothing: a link is an annotation rather than ink, so a linked title and a plain one
are the same sheet, which is also why the parity gates cannot see it and a test
asserts the targets directly. The six-argument constructor is kept explicitly, so
existing calls compile and link unchanged.

- **`CvEntry` carries a location and a mark, and gains a builder.** The record held a
title, a subtitle, a date and a body, which is enough for a dated block and not
enough for the designs that set the city beside the employer in its own colour, or
Expand Down Expand Up @@ -59,6 +68,17 @@ follow semantic versioning; release dates are ISO 8601.

### Templates

- **`SerifHeadline` links its titles and lets its bands breathe.** Every title the
preset draws — a role, a project, a degree, an achievement — is now a link when its
entry carries one. And a band column keeps a gutter at its right edge, so its text
stops short of the hairline between columns instead of running into it: **the first
place a promoted preset deliberately departs from the sheet it ports**, where a line
that happens to fill its column touches the rule. The departure is exactly measured —
six of 235 nodes narrow, 8 191 of 2 173 720 pixels change, and nothing else moves —
and both baselines were re-recorded with it. The class documentation names the two
colours the packaged marks come in, so a document chooses one deliberately rather
than mixing navy and gold in a row by accident.

- **A two-column editorial CV preset: `SerifHeadline`.** A one-page A4 sheet under a
Volkhov masthead — the name in the display serif over its role and a short gold rule,
the contact channels stacked opposite — then a two-column body: the roles held on a
Expand Down
Binary file modified assets/readme/examples/cv-serif-headline-v2.pdf
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
*
* <p>The project and achievement marks are this preset's own vocabulary —
* {@code cart}, {@code api}, {@code trophy}, {@code chart}, {@code rocket} —
* and each entry names the one it wants.</p>
* and each entry names the one it wants. The marks come in two colours:
* {@code cart} and {@code api} are drawn in the navy of the design's ink,
* the other three in its gold. This sample keeps a band to one colour, since
* mixing them inside one row reads as a mistake rather than a choice.</p>
*/
public final class SerifHeadlineSampleData {

Expand Down Expand Up @@ -98,13 +101,15 @@ public static CvDocument sample() {
.subtitle("Java, PostgreSQL, Testcontainers")
.body("Open-source double-entry ledger for JVM services, with a"
+ " property-based suite over the posting rules.")
.icon("api")
.icon("chart")
.link("https://github.com/thalvorsen/ledgerkit")
.build())
.entry(CvEntry.builder("Runbook Digest")
.subtitle("Kotlin, Kafka, OpenSearch")
.body("Reads alert history and drafts the retrospective agenda"
+ " before the meeting starts.")
.icon("chart")
.icon("rocket")
.link("https://github.com/thalvorsen/runbook-digest")
.build())
.build())
.section(EntriesSection.builder("Education")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,49 @@
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Pins the compatibility promise made when {@link CvEntry} grew a location
* and a mark: the four-argument constructor that predates them is still
* there and still means what it meant, so a caller written against it keeps
* compiling and linking.
* Pins the compatibility promise made each time {@link CvEntry} grew: the
* constructors that predate the location, the mark and the link are still
* there and still mean what they meant, so a caller written against any of
* them keeps compiling and linking.
*/
class CvEntryPlaceAndIconTest {

@Test
void theConstructorThatPredatesThemLeavesBothBlank() {
void theConstructorThatPredatesThemLeavesThemAllBlank() {
CvEntry entry = new CvEntry("Engineer", "Acme", "2021", "Did the work.");
assertThat(entry.place()).isEmpty();
assertThat(entry.icon()).isEmpty();
assertThat(entry.link()).isEmpty();
assertThat(entry.subtitle()).isEqualTo("Acme");
}

@Test
void theConstructorThatPredatesTheLinkLeavesItBlank() {
CvEntry entry = new CvEntry("Engineer", "Acme", "2021", "Body", "Berlin", "cart");
assertThat(entry.link()).isEmpty();
assertThat(entry.place()).isEqualTo("Berlin");
assertThat(entry.icon()).isEqualTo("cart");
}

@Test
void nullsNormalizeToBlank() {
CvEntry entry = new CvEntry("Engineer", "Acme", "2021", "Body", null, null);
CvEntry entry = new CvEntry("Engineer", "Acme", "2021", "Body", null, null, null);
assertThat(entry.place()).isEmpty();
assertThat(entry.icon()).isEmpty();
assertThat(entry.link()).isEmpty();
}

@Test
void theOriginalFieldsStillRejectNull() {
assertThatThrownBy(() -> new CvEntry("Engineer", null, "2021", "Body", "Berlin", "cart"))
assertThatThrownBy(() ->
new CvEntry("Engineer", null, "2021", "Body", "Berlin", "cart", ""))
.isInstanceOf(NullPointerException.class)
.hasMessageContaining("subtitle");
}

@Test
void aBlankTitleIsStillRejected() {
assertThatThrownBy(() -> new CvEntry(" ", "Acme", "2021", "Body", "", ""))
assertThatThrownBy(() -> new CvEntry(" ", "Acme", "2021", "Body", "", "", ""))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("title");
}
Expand All @@ -52,9 +63,11 @@ void theBuilderCarriesEveryFieldThrough() {
.body("An open-source ledger.")
.place("Remote")
.icon("cart")
.link("https://example.com/ledgerkit")
.build();
assertThat(entry).isEqualTo(new CvEntry("Ledgerkit", "Java, PostgreSQL", "2024",
"An open-source ledger.", "Remote", "cart"));
"An open-source ledger.", "Remote", "cart",
"https://example.com/ledgerkit"));
}

@Test
Expand All @@ -65,6 +78,7 @@ void theBuilderLeavesWhatItIsNotToldBlank() {
assertThat(entry.body()).isEmpty();
assertThat(entry.place()).isEmpty();
assertThat(entry.icon()).isEmpty();
assertThat(entry.link()).isEmpty();
}

@Test
Expand All @@ -85,9 +99,11 @@ void theBuilderTreatsNullsAsBlank() {
.body((String) null)
.place(null)
.icon(null)
.link(null)
.build();
assertThat(entry.subtitle()).isEmpty();
assertThat(entry.body()).isEmpty();
assertThat(entry.place()).isEmpty();
assertThat(entry.link()).isEmpty();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,43 @@ void rejectsAnUnknownEntryMarkByName() {
}
}

@Test
void anEntryWithALinkReachesThePdfAsOne() throws Exception {
// Every title this preset draws is a link when its entry carries
// one. It is an annotation, so it moves no pixel and no layout node
// — which is why neither parity gate can see it.
byte[] pdf = render(CvDocument.builder()
.identity(identity())
.section(EntriesSection.builder("Projects")
.entry(CvEntry.builder("Ledgerkit")
.subtitle("Java")
.body("A ledger.")
.icon("api")
.link("https://example.com/ledgerkit")
.build())
.build())
.section(EntriesSection.builder("Experience")
.entry(CvEntry.builder("Engineer")
.subtitle("Acme")
.date("2024")
.body("Shipped a thing.")
.link("https://acme.example.com")
.build())
.build())
.section(EntriesSection.builder("Achievements")
.entry(CvEntry.builder("Engineer of the Year")
.body("For the platform work.")
.icon("trophy")
.link("https://example.com/award")
.build())
.build())
.build());
assertThat(linkUris(pdf))
.contains("https://example.com/ledgerkit",
"https://acme.example.com",
"https://example.com/award");
}

@Test
void drawsAnEntryWithoutAMark() throws Exception {
// No token, no plate — the card is drawn, just unmarked.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1705,15 +1705,15 @@
"computedY" : 218.426,
"placementX" : 31.682,
"placementY" : 218.426,
"placementWidth" : 168.307,
"placementWidth" : 161.798,
"placementHeight" : 52.675,
"startPage" : 0,
"endPage" : 0,
"contentWidth" : 168.307,
"contentWidth" : 161.798,
"contentHeight" : 52.675,
"margin" : {
"top" : 0.0,
"right" : 171.488,
"right" : 177.301,
"bottom" : 0.0,
"left" : 0.0
},
Expand Down Expand Up @@ -1795,11 +1795,11 @@
"computedY" : 218.426,
"placementX" : 64.818,
"placementY" : 218.426,
"placementWidth" : 135.172,
"placementWidth" : 128.662,
"placementHeight" : 27.484,
"startPage" : 0,
"endPage" : 0,
"contentWidth" : 135.172,
"contentWidth" : 128.662,
"contentHeight" : 27.484,
"margin" : {
"top" : 0.0,
Expand All @@ -1825,15 +1825,15 @@
"computedY" : 218.426,
"placementX" : 203.17,
"placementY" : 218.426,
"placementWidth" : 167.99,
"placementWidth" : 164.738,
"placementHeight" : 52.675,
"startPage" : 0,
"endPage" : 0,
"contentWidth" : 167.99,
"contentWidth" : 164.738,
"contentHeight" : 52.675,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"right" : 5.813,
"bottom" : 0.0,
"left" : 171.488
},
Expand Down Expand Up @@ -1915,11 +1915,11 @@
"computedY" : 218.426,
"placementX" : 244.444,
"placementY" : 218.426,
"placementWidth" : 126.716,
"placementWidth" : 123.464,
"placementHeight" : 27.484,
"startPage" : 0,
"endPage" : 0,
"contentWidth" : 126.716,
"contentWidth" : 123.464,
"contentHeight" : 27.484,
"margin" : {
"top" : 0.0,
Expand Down Expand Up @@ -5193,7 +5193,7 @@
"contentHeight" : 26.204,
"margin" : {
"top" : 0.0,
"right" : 425.529,
"right" : 431.342,
"bottom" : 0.0,
"left" : 0.0
},
Expand Down Expand Up @@ -5283,7 +5283,7 @@
"contentHeight" : 26.204,
"margin" : {
"top" : 0.0,
"right" : 319.147,
"right" : 324.96,
"bottom" : 0.0,
"left" : 106.382
},
Expand Down Expand Up @@ -5373,7 +5373,7 @@
"contentHeight" : 26.204,
"margin" : {
"top" : 0.0,
"right" : 212.765,
"right" : 218.578,
"bottom" : 0.0,
"left" : 212.765
},
Expand Down Expand Up @@ -5463,7 +5463,7 @@
"contentHeight" : 26.204,
"margin" : {
"top" : 0.0,
"right" : 106.382,
"right" : 112.196,
"bottom" : 0.0,
"left" : 319.147
},
Expand Down Expand Up @@ -5553,7 +5553,7 @@
"contentHeight" : 26.204,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"right" : 5.813,
"bottom" : 0.0,
"left" : 425.529
},
Expand Down Expand Up @@ -6513,7 +6513,7 @@
"contentHeight" : 36.181,
"margin" : {
"top" : 0.0,
"right" : 354.608,
"right" : 360.421,
"bottom" : 0.0,
"left" : 0.0
},
Expand Down Expand Up @@ -6595,15 +6595,15 @@
"computedY" : 43.938,
"placementX" : 208.986,
"placementY" : 43.938,
"placementWidth" : 173.765,
"placementWidth" : 166.571,
"placementHeight" : 36.181,
"startPage" : 0,
"endPage" : 0,
"contentWidth" : 173.765,
"contentWidth" : 166.571,
"contentHeight" : 36.181,
"margin" : {
"top" : 0.0,
"right" : 177.304,
"right" : 183.117,
"bottom" : 0.0,
"left" : 177.304
},
Expand Down Expand Up @@ -6655,11 +6655,11 @@
"computedY" : 43.938,
"placementX" : 249.097,
"placementY" : 43.938,
"placementWidth" : 133.653,
"placementWidth" : 126.46,
"placementHeight" : 17.849,
"startPage" : 0,
"endPage" : 0,
"contentWidth" : 133.653,
"contentWidth" : 126.46,
"contentHeight" : 17.849,
"margin" : {
"top" : 0.0,
Expand Down Expand Up @@ -6693,7 +6693,7 @@
"contentHeight" : 36.181,
"margin" : {
"top" : 0.0,
"right" : 0.0,
"right" : 5.813,
"bottom" : 0.0,
"left" : 354.608
},
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading