diff --git a/CHANGELOG.md b/CHANGELOG.md index f04c042f6..cef198cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -96,7 +96,8 @@ follow semantic versioning; release dates are ISO 8601. terracotta square and no dash under its heading, the other a disc and a dash, which is how the sheet tells two lists of one-liners apart. Each fact and each project takes the mark its entry names in `CvEntry.icon()` from this preset's own vocabulary, - a project title is a link when its entry carries one, and the monogram is drawn from + every title it draws — a role, a project, a degree, a credential — is a link when its + entry carries one, and the monogram is drawn from the name's own initials rather than a field of its own — a document states its name once, and a monogram that could disagree with it would be a second place to keep true. **A link in the contact block is drawn as its own label with the address behind @@ -111,8 +112,8 @@ follow semantic versioning; release dates are ISO 8601. `AtomicNodeTooLargeException` rather than flowing or dropping entries. Guarded by a smoke test (including the unknown-mark data error, an entry with no mark, an identity with no links, a document with nothing but an identity, the monogram, the link - targets, the four contact rows sharing one axis and the one-page limit), an exact - layout snapshot and a pixel-parity gate; + targets on every kind of title, the four contact rows sharing one axis and the + one-page limit), an exact layout snapshot and a pixel-parity gate; the examples showcase gains `cv-terracotta-rail-v2`. - **The first invoice preset that paginates what it ports: `LumaStudioInvoice`.** A diff --git a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailSmokeTest.java b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailSmokeTest.java index 1cbf9fc03..86332096f 100644 --- a/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailSmokeTest.java +++ b/qa/src/test/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailSmokeTest.java @@ -173,6 +173,46 @@ void everyContactRowStartsOnTheSameAxis() throws Exception { } } + @Test + void everyTitleBecomesALinkWhenItsEntryCarriesOne() throws Exception { + // A role, a degree and a credential, each pointing somewhere. None of + // it moves a pixel or a layout node, so this is the only thing that + // would notice the annotations going missing. + List placements = new ArrayList<>(); + for (CvDocument.Placement placement : TerracottaRailFixtures.canonicalCv().placements()) { + placements.add(switch (placement.section().title()) { + case "PROFESSIONAL EXPERIENCE" -> new CvDocument.Placement(Slot.MAIN, + new EntriesSection("PROFESSIONAL EXPERIENCE", List.of( + CvEntry.builder("Senior Architect") + .subtitle("Northline Studio, Bristol, UK") + .date("2021") + .link("https://example.test/northline") + .body("Lead design packages.") + .build()))); + case "EDUCATION" -> new CvDocument.Placement(Slot.MAIN, + new EntriesSection("EDUCATION", List.of( + CvEntry.builder("MArch Architecture") + .subtitle("University of Sheffield") + .date("2014") + .link("https://example.test/sheffield") + .build()))); + case "CERTIFICATIONS" -> new CvDocument.Placement(Slot.SIDEBAR, + new EntriesSection("CERTIFICATIONS", List.of( + CvEntry.builder("ARB Registered Architect") + .link("https://example.test/arb") + .build()))); + default -> placement; + }); + } + + List targets = linkTargets( + render(new CvDocument(TerracottaRailFixtures.identity(), placements))); + assertThat(targets) + .contains("https://example.test/northline") + .contains("https://example.test/sheffield") + .contains("https://example.test/arb"); + } + @Test void aProjectTitleBecomesALinkWhenItsEntryCarriesOne() throws Exception { EntriesSection linked = new EntriesSection("SELECTED PROJECTS", List.of( diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRail.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRail.java index 06dd1b06e..2dc44a148 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRail.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRail.java @@ -72,9 +72,15 @@ * {@code CvEntry.icon()}, from this preset's own vocabulary — * {@code globe}, {@code badge}, {@code clock}, {@code building}, * {@code hotel}, {@code house} — and an unknown token is reported as a data - * error naming the set. An entry with no token is drawn without a mark, and - * a project title becomes a link when its entry carries - * {@code CvEntry.link()}.

+ * error naming the set. An entry with no token is drawn without a mark.

+ * + *

Every title the preset draws — a role, a project, a degree, a + * credential — becomes a link when its entry carries {@code CvEntry.link()}. + * It costs the layout nothing, because a link is an annotation rather than + * ink, so a linked title and a plain one are the same line. The closing facts + * are the exception: their bold line is a label for the values under it — + * "Languages:", "Availability:" — rather than the name of something a reader + * could open.

* *

The email, the phone and each link are reachable from the PDF, with the * {@code mailto:} and {@code tel:} targets built from the values. A link is diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java index b9a8aa33e..50e82e123 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailAside.java @@ -213,12 +213,12 @@ private static void renderBulletedSkills(SectionBuilder side, SkillsSection sect String name = prefix + "_" + index++; if (discBullet) { bulletLine(block, name, p -> p.dot(3.0, ACCENT), skill.name(), - INK, DETAIL_SIZE); + INK, DETAIL_SIZE, null); } else { bulletLine(block, name, p -> inlineIcon(p, TerracottaRailIcons.SQUARE, TerracottaRailIcons.BULLET_SIZE), - skill.name(), INK, DETAIL_SIZE); + skill.name(), INK, DETAIL_SIZE, null); } } } @@ -232,8 +232,9 @@ private static void renderCertifications(SectionBuilder side, EntriesSection sec headingWithDash(block, section.title(), SIDEBAR_HEADING_SPACER, SIDEBAR_DASH_WIDTH); List entries = section.entries(); for (int index = 0; index < entries.size(); index++) { + CvEntry entry = entries.get(index); bulletLine(block, "Certification_" + index, p -> p.dot(3.0, ACCENT), - entries.get(index).title(), INK, DETAIL_SIZE); + entry.title(), INK, DETAIL_SIZE, entry.link()); } }); } diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java index 985c12538..c9bc38fc4 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailMain.java @@ -172,7 +172,7 @@ private static void renderExperience(SectionBuilder main, EntriesSection experie body.padding(0f, 0f, last ? 0f : (float) ENTRY_GAP, (float) ENTRY_INDENT); railedLine(body, "Role", index, titleAndDate("RoleTable_" + index, entry.title(), entry.date(), - ENTRY_WIDTH, ROLE_PERIOD_SHARE)); + ENTRY_WIDTH, ROLE_PERIOD_SHARE, entry.link())); body.addParagraph(p -> p .name("Employer_" + index) .text(entry.subtitle()) @@ -301,7 +301,8 @@ private static void renderEducation(SectionBuilder main, EntriesSection educatio (float) ENTRY_INDENT); railedLine(body, "Edu", index, titleAndDate("EduTable_" + index, entry.title(), entry.date(), - EDUCATION_ENTRY_WIDTH, EDUCATION_PERIOD_SHARE)); + EDUCATION_ENTRY_WIDTH, EDUCATION_PERIOD_SHARE, + entry.link())); body.addParagraph(p -> p .name("Institution_" + index) .text(entry.subtitle()) diff --git a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java index 590883f1a..4a70b3b2c 100644 --- a/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java +++ b/templates/src/main/java/com/demcha/compose/document/templates/cv/presets/TerracottaRailWidgets.java @@ -5,6 +5,7 @@ import com.demcha.compose.document.dsl.RowBuilder; import com.demcha.compose.document.dsl.SectionBuilder; import com.demcha.compose.document.dsl.TableBuilder; +import com.demcha.compose.document.node.DocumentLinkOptions; import com.demcha.compose.document.node.DocumentNode; import com.demcha.compose.document.node.InlineImageAlignment; import com.demcha.compose.document.node.LayerAlign; @@ -194,10 +195,30 @@ static void railedLine(SectionBuilder body, String prefix, int index, DocumentNo * — it spans the cell and its columns lay out. Both cells hold a * paragraph, because a table inside a row cell draws only leaf * content.

+ * + *

The title carries the entry's link when it has one. That costs the + * layout nothing — a link is an annotation rather than ink — so a linked + * title and a plain one are the same line.

+ * + * @param name the node name this line's parts are built from + * @param title the entry's title + * @param date the date, set flush right + * @param width the line's width + * @param dateShare how much of that width the date takes + * @param link the target the title points at, blank for none + * @return the line node */ static DocumentNode titleAndDate(String name, String title, String date, - double width, double dateShare) { + double width, double dateShare, String link) { double dateColumn = width * dateShare; + ParagraphBuilder titleCell = new ParagraphBuilder() + .name(name + "_Title") + .text(title) + .lineSpacing(0) + .textStyle(text(ITEM_TITLE_SIZE, INK, true)); + if (link != null && !link.isBlank()) { + titleCell.link(new DocumentLinkOptions(link)); + } return new TableBuilder() .name(name) .width(width) @@ -209,12 +230,7 @@ static DocumentNode titleAndDate(String name, String title, String date, .stroke(NO_BORDER) .build()) .rowCells( - DocumentTableCell.node(new ParagraphBuilder() - .name(name + "_Title") - .text(title) - .lineSpacing(0) - .textStyle(text(ITEM_TITLE_SIZE, INK, true)) - .build()), + DocumentTableCell.node(titleCell.build()), DocumentTableCell.node(new ParagraphBuilder() .name(name + "_Period") .text(date) @@ -246,14 +262,21 @@ static void layeredRow(SectionBuilder parent, String name, double marginTop, .layer(layer.build(), LayerAlign.TOP_LEFT, 0)); } - /** A bullet line: a mark, a gap, and the item beside it. */ + /** + * A bullet line: a mark, a gap, and the item beside it — carrying a link + * when one is given. + */ static void bulletLine(SectionBuilder block, String name, Consumer mark, - String item, DocumentColor color, double size) { + String item, DocumentColor color, double size, String link) { block.addParagraph(p -> { p.name(name); mark.accept(p); p.inlineText(" "); - p.inlineText(item, text(size, color, false)); + if (link == null || link.isBlank()) { + p.inlineText(item, text(size, color, false)); + } else { + p.inlineText(item, text(size, color, false), new DocumentLinkOptions(link)); + } p.margin(0f, 0f, (float) BULLET_ROW_GAP, 0f); }); }