Skip to content

Commit 609cf7a

Browse files
authored
v1.6.4: Boxed Sections parser + structured WorkHistory/Education blocks + bold KeyValue
1 parent ef23b12 commit 609cf7a

17 files changed

Lines changed: 2936 additions & 120 deletions

File tree

CHANGELOG.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,80 @@
33
All notable changes to GraphCompose are documented here. Versions
44
follow semantic versioning; release dates are ISO 8601.
55

6+
## v1.6.4 — Planned
7+
8+
Bug fix + structured-block patch. Adds two new public Block types —
9+
`WorkHistoryBlock` and `EducationBlock` — that let template authors
10+
declare work-history and education entries with explicit (title,
11+
organisation, date, description) / (degree, institution, year,
12+
details) fields instead of relying on the legacy
13+
`MultiParagraphBlock` pipe-separated string parser. Also closes a
14+
Boxed Sections layout defect that bundled the date and description
15+
into the right-aligned date column for any author-supplied line that
16+
used an em-dash (`" — "`), en-dash (`" – "`), or contained
17+
prose-shaped content the parser misread as a date. **No public API
18+
break** — the sealed `Block` permit list grows from six to eight,
19+
existing `MultiParagraphBlock` work-history strings continue to
20+
parse, and the deprecated parser path stays in place for backward
21+
compatibility.
22+
23+
### Templates — new structured blocks
24+
25+
- **`WorkHistoryBlock`.** New public record block carrying a list of
26+
`Item(title, organisation, date, description)` entries. The
27+
`BoxedSections` preset renders each item as a structured row:
28+
title bold on the left, date right-aligned on the same row,
29+
organisation italic on the next line under the title, and
30+
description as a full-width paragraph beneath. Other presets fall
31+
back to a single concatenated paragraph per item. Authors who use
32+
`WorkHistoryBlock` bypass the legacy
33+
`BoxedSections#parseWorkEntry` heuristic parser entirely.
34+
- **`EducationBlock`.** New public record block carrying a list of
35+
`Item(degree, institution, year, details)` entries. Renders with
36+
the same structured layout as `WorkHistoryBlock` (degree bold
37+
left, year right, institution italic, details paragraph) so
38+
Education & Certifications sections visually match Professional
39+
Experience.
40+
- **Sample data migrated.** `ExampleDataFactory.sampleCvSpecV2` now
41+
uses `WorkHistoryBlock` for Professional Experience and
42+
`EducationBlock` for Education & Certifications. The legacy
43+
`MultiParagraphBlock` pattern remains supported and is exercised
44+
by `PresetLayoutSnapshotTest` / `PresetVisualParityTest` to lock
45+
the backward-compat path.
46+
47+
### Templates — parser robustness (legacy path)
48+
49+
- **`parseWorkEntry` accepts em-dash and en-dash.** Used to split
50+
the post-pipe segment on ASCII `" - "` only; now tries `" — "`,
51+
`" – "`, and `" - "` in order, mirroring `splitHeading`. Authors
52+
who typed `"*2024-Present* — Led reusable document flows."` saw
53+
the whole tail collapse into the date column — this no longer
54+
happens.
55+
- **`parseWorkEntry` rejects prose dressed up as a date.** The
56+
loose `looksLikeDate` check accepted any string containing a
57+
year and a hyphen anywhere, which caused education lines like
58+
`"... | 2019. First-class honours. Specialisation ..."` to
59+
parse as work entries (the hyphen inside `"First-class"` was
60+
enough to satisfy the heuristic). Parser now rejects post-pipe
61+
segments that contain sentence-ending punctuation (`.`, `:`,
62+
`;`) when no explicit date / description separator was found,
63+
letting these lines fall back to plain paragraph rendering.
64+
Marked `@Deprecated` with a `@deprecated` Javadoc pointing
65+
callers to `WorkHistoryBlock` / `EducationBlock`.
66+
- **`parseProjectItem`** picks up the same em-dash / en-dash /
67+
ASCII separator set so future Project items typed with em-dash
68+
don't regress into "title only" rendering.
69+
70+
### Tests
71+
72+
- `BlockTest.blockSealingPermitsAllEightVariants` updated for the
73+
two new permitted block types.
74+
- `PresetVisualGalleryTest.sampleSpec` migrated to
75+
`WorkHistoryBlock` so the visible "primary example" exercises the
76+
new structured shape.
77+
- `PresetLayoutSnapshotTest` intentionally retained on
78+
`MultiParagraphBlock` to lock the legacy parser's behaviour.
79+
680
## v1.6.3 — 2026-05-22
781

882
Bug fix patch. Closes two independent hyperlink clickable-area

examples/src/main/java/com/demcha/examples/support/ExampleDataFactory.java

Lines changed: 62 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package com.demcha.examples.support;
22

33
import com.demcha.compose.document.templates.blocks.BulletListBlock;
4+
import com.demcha.compose.document.templates.blocks.EducationBlock;
45
import com.demcha.compose.document.templates.blocks.IndentedBlock;
56
import com.demcha.compose.document.templates.blocks.KeyValueBlock;
67
import com.demcha.compose.document.templates.blocks.MultiParagraphBlock;
78
import com.demcha.compose.document.templates.blocks.ParagraphBlock;
9+
import com.demcha.compose.document.templates.blocks.WorkHistoryBlock;
810
import com.demcha.compose.document.templates.coverletter.spec.CoverLetterHeader;
911
import com.demcha.compose.document.templates.coverletter.spec.CoverLetterSpec;
1012
import com.demcha.compose.document.templates.cv.spec.CvHeader;
@@ -286,16 +288,32 @@ public static CvSpec sampleCvSpecV2() {
286288
"**Distribution:** Maven Central, Sonatype OSSRH, GPG signing, "
287289
+ "JitPack, semantic versioning discipline"))))
288290
.module(CvModule.of("Education & Certifications",
289-
new MultiParagraphBlock(List.of(
290-
"**MSc Computer Science** - University of Manchester | 2021. "
291-
+ "Distinction. Thesis: *Composable layout primitives for "
292-
+ "deterministic document rendering*.",
293-
"**BSc Software Engineering** - Imperial College London | 2019. "
294-
+ "First-class honours. Specialisation in compilers and "
295-
+ "static analysis.",
296-
"**Oracle Java Certification** - Professional track | 2023. "
297-
+ "Java 17 platform deep-dive: records, sealed types, "
298-
+ "pattern matching, virtual threads."))))
291+
// Preferred: structured EducationBlock with
292+
// explicit (degree, institution, year, details)
293+
// fields. BoxedSections renders each item with
294+
// the same structured layout as Professional
295+
// Experience — degree bold left, year right,
296+
// institution italic on the next line, and
297+
// details as a full-width paragraph below.
298+
new EducationBlock(List.of(
299+
new EducationBlock.Item(
300+
"MSc Computer Science",
301+
"University of Manchester",
302+
"2021",
303+
"Distinction. Thesis: *Composable layout primitives "
304+
+ "for deterministic document rendering*."),
305+
new EducationBlock.Item(
306+
"BSc Software Engineering",
307+
"Imperial College London",
308+
"2019",
309+
"First-class honours. Specialisation in compilers and "
310+
+ "static analysis."),
311+
new EducationBlock.Item(
312+
"Oracle Java Certification",
313+
"Professional track",
314+
"2023",
315+
"Java 17 platform deep-dive: records, sealed types, "
316+
+ "pattern matching, virtual threads.")))))
299317
.module(CvModule.of("Projects",
300318
new BulletListBlock(List.of(
301319
"**GraphCompose (Java 21, PDFBox, Maven, JMH)** - "
@@ -316,23 +334,40 @@ public static CvSpec sampleCvSpecV2() {
316334
+ "GraphCompose: cinematic covers, pull quotes, "
317335
+ "multi-column flow, sidebar callouts."))))
318336
.module(CvModule.of("Professional Experience",
319-
new MultiParagraphBlock(List.of(
320-
"**Senior Platform Engineer**, Northwind Systems | "
321-
+ "*2024-Present* - Led the reusable document-generation "
322-
+ "platform serving billing, hiring, and reporting flows "
323-
+ "across **8 product teams**. Reduced template "
324-
+ "maintenance time by **70%** by retiring per-team "
325-
+ "PDF scripts in favour of one canonical engine.",
326-
"**Software Engineer**, BrightLeaf Labs | *2021-2024* - Built "
327-
+ "backend services and production document rendering "
328-
+ "pipelines processing **2M+ documents per month**. "
329-
+ "Drove the migration from iText to a custom layout "
330-
+ "engine, eliminating licensing risk and cutting "
331-
+ "p99 render latency from 1.4s to 380ms.",
332-
"**Backend Engineer**, Helix Print Co | *2019-2021* - "
333-
+ "Maintained a high-volume invoice-printing service "
334-
+ "(15M PDFs/year) and authored the compliance test "
335-
+ "harness that gated every template change."))))
337+
// Preferred: structured WorkHistoryBlock with
338+
// explicit (title, organisation, date,
339+
// description) fields. BoxedSections renders
340+
// each item as a structured row (title bold
341+
// left, date right, organisation italic on the
342+
// next line, description full-width below)
343+
// without falling back to the legacy
344+
// pipe-separated string parser.
345+
new WorkHistoryBlock(List.of(
346+
new WorkHistoryBlock.Item(
347+
"Senior Platform Engineer",
348+
"Northwind Systems",
349+
"2024-Present",
350+
"Led the reusable document-generation platform serving "
351+
+ "billing, hiring, and reporting flows across "
352+
+ "**8 product teams**. Reduced template maintenance "
353+
+ "time by **70%** by retiring per-team PDF scripts "
354+
+ "in favour of one canonical engine."),
355+
new WorkHistoryBlock.Item(
356+
"Software Engineer",
357+
"BrightLeaf Labs",
358+
"2021-2024",
359+
"Built backend services and production document rendering "
360+
+ "pipelines processing **2M+ documents per month**. "
361+
+ "Drove the migration from iText to a custom layout "
362+
+ "engine, eliminating licensing risk and cutting "
363+
+ "p99 render latency from 1.4s to 380ms."),
364+
new WorkHistoryBlock.Item(
365+
"Backend Engineer",
366+
"Helix Print Co",
367+
"2019-2021",
368+
"Maintained a high-volume invoice-printing service "
369+
+ "(15M PDFs/year) and authored the compliance test "
370+
+ "harness that gated every template change.")))))
336371
.module(CvModule.of("Additional Information",
337372
new KeyValueBlock(List.of(
338373
new KeyValueBlock.Entry("Languages",

src/main/java/com/demcha/compose/document/templates/blocks/Block.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
*
1212
* <p>The sealed permit list is intentionally exhaustive: every body
1313
* shape that a CV / cover-letter / invoice / proposal preset can
14-
* declare today is one of the six concrete records. To add a new body
15-
* shape, extend the {@code permits} list and update the Module
14+
* declare today is one of the eight concrete records. To add a new
15+
* body shape, extend the {@code permits} list and update the Module
1616
* composer to handle the new variant.</p>
1717
*
1818
* <p>Block records are immutable and safe to reuse across documents.</p>
@@ -23,5 +23,7 @@ public sealed interface Block
2323
NumberedListBlock,
2424
IndentedBlock,
2525
KeyValueBlock,
26-
MultiParagraphBlock {
26+
MultiParagraphBlock,
27+
WorkHistoryBlock,
28+
EducationBlock {
2729
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package com.demcha.compose.document.templates.blocks;
2+
3+
import java.util.List;
4+
import java.util.Objects;
5+
6+
/**
7+
* A {@link Block} that captures a stack of education / certification
8+
* entries with each field (degree, institution, year, details)
9+
* supplied separately so presets can place them precisely without
10+
* re-parsing a concatenated source string.
11+
*
12+
* <p>This is the <strong>preferred shape</strong> for "Education",
13+
* "Education &amp; Certifications", "Qualifications" or any module
14+
* whose body is a list of degree / course entries. The
15+
* {@code BoxedSections} preset renders each {@link Item} with the
16+
* same structured layout as {@code WorkHistoryBlock}: degree bold on
17+
* the left, year right-aligned on the same row, institution italic
18+
* on the next line under the degree, and details as a full-width
19+
* paragraph beneath. Other presets fall back to a single inline
20+
* paragraph per item.</p>
21+
*
22+
* <p><strong>Legacy alternative.</strong> Authors may still pass
23+
* education as a {@link MultiParagraphBlock} of pipe-separated
24+
* strings — e.g.
25+
* {@code "**Degree** - Institution | Year. Details..."} — and the
26+
* legacy parser tries to interpret them. Prefer
27+
* {@code EducationBlock} in new code: the structured fields are
28+
* explicit, do not depend on the parser's separator and date
29+
* heuristics (which over-trigger on prose containing stray hyphens
30+
* like "First-class"), and survive copy-paste from spreadsheets
31+
* without quoting concerns.</p>
32+
*
33+
* @param items education entries in source order, most-recent-first
34+
* by convention (must not be null; may be empty;
35+
* individual items must not be null)
36+
*/
37+
public record EducationBlock(List<Item> items) implements Block {
38+
39+
/**
40+
* Compact constructor that defensively copies the supplied list and
41+
* validates that no item reference is null.
42+
*
43+
* @throws NullPointerException if {@code items} or any element is
44+
* null
45+
*/
46+
public EducationBlock {
47+
Objects.requireNonNull(items, "items");
48+
items = List.copyOf(items);
49+
}
50+
51+
/**
52+
* One row in an education stack. All four fields are required
53+
* non-null strings but may be blank — a blank {@code institution}
54+
* collapses the subtitle line, a blank {@code details} collapses
55+
* the body paragraph, and a blank {@code year} renders the degree
56+
* row without a right-aligned year column.
57+
*
58+
* @param degree degree / qualification name, e.g.
59+
* {@code "MSc Computer Science"} or
60+
* {@code "Oracle Java Certification"}
61+
* @param institution awarding institution, e.g.
62+
* {@code "University of Manchester"} or
63+
* {@code "Professional track"}
64+
* @param year year or year range, e.g. {@code "2021"},
65+
* {@code "2018-2021"}
66+
* @param details additional details (honours, thesis,
67+
* specialisation, course content); free prose,
68+
* may contain inline markdown
69+
* ({@code **bold**}, {@code *italic*})
70+
*/
71+
public record Item(String degree, String institution, String year, String details) {
72+
73+
/**
74+
* Compact constructor: rejects null fields. Use empty strings
75+
* for absent values rather than null.
76+
*/
77+
public Item {
78+
Objects.requireNonNull(degree, "degree");
79+
Objects.requireNonNull(institution, "institution");
80+
Objects.requireNonNull(year, "year");
81+
Objects.requireNonNull(details, "details");
82+
}
83+
}
84+
}

0 commit comments

Comments
 (0)