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

### Documentation

- **A committed preview can be reproduced from a branch that has moved past it.** The
documents that print a version took it from the reactor, which between releases sits on
the next patch — so a render from `develop` named a version nobody could depend on yet,
and stripping the `-SNAPSHOT` never helped because the number itself had moved. The
version to display can now be passed in; `banner.properties` still sources
`@project.version@`, so the reactor remains the answer when nobody says otherwise. Two
headers stopped carrying the `{date}` token, which the engine resolves against the wall
clock and which made the same document differ by the day it was rendered — the token
stays in the example whose subject is the token. The number also left the places that
merely decorated with it, a hero kicker and two footers among them, and stays where it
informs: the version pill and the Maven coordinate card, which now reads the same input
as everything else instead of a string literal.
- **The example catalogue is built one way, and it starts empty.** The runner only ever
wrote, so a renamed or deleted example left its old document behind — published to the
site, counted by the guards that read the tree, and indistinguishable from a current
Expand Down
Binary file modified assets/readme/examples/engine-deck-v2.pdf
Binary file not shown.
Binary file modified assets/readme/examples/engine-deck.pdf
Binary file not shown.
Binary file modified assets/readme/examples/feature-catalog.pdf
Binary file not shown.
Binary file modified assets/readme/examples/master-showcase.pdf
Binary file not shown.
Binary file modified assets/readme/examples/master-showcase.pptx
Binary file not shown.
Binary file modified assets/readme/repository_showcase_render.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 16 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,24 @@ Then run every example in one shot:
-Dexec.mainClass=com.demcha.examples.flagships.MasterShowcaseExample
```

Generated PDFs land in `examples/target/generated-pdfs/`. The same
Generated PDFs land in `examples/target/generated-pdfs/`. The runner empties that
directory first, so what is left is exactly what this run produced. The same
`mvnw.cmd` form works on Windows PowerShell with backslash paths.

The documents that print a version take it from the reactor, which between releases
sits on the next patch. To reproduce a published document from a development branch,
name the published version:

```bash
./mvnw -f examples/pom.xml exec:java \
-Dexec.mainClass=com.demcha.examples.GenerateAllExamples \
-Dgraphcompose.examples.displayVersion=2.1.0
```

That is how the committed previews under `assets/readme/examples/` are produced — the
version they show is the one on Maven Central, not the one the branch is building
towards.

`GenerateAllExamples` renders the whole catalogue in one pass — the CV and
cover-letter presets plus invoices, proposals, a schedule, the feature
demos, and the flagships. The showcase site publishes the whole generated
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ static void compose(DocumentSession document) {

document.footer(DocumentHeaderFooter.builder()
.zone(DocumentHeaderFooterZone.FOOTER)
.leftText("GraphCompose · v" + VERSION + " “" + CODENAME + "”")
.leftText("GraphCompose · “" + CODENAME + "”")
.rightText("Page {page} of {pages}")
.fontSize(8.5f)
.textColor(MUTED)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,15 +115,6 @@ public final class EngineDeckV2Example {

private static final String VERSION = ExampleVersion.withoutQualifier();

/**
* The {@code major.minor} of {@link #VERSION}, for the banner's prose labels.
*
* <p>Derived rather than written out: the banner is regenerated on every release,
* so a literal would keep announcing whichever line it was typed on — the hero
* still read "2.0" while the pill beside it read v2.1.0.</p>
*/
private static final String VERSION_LINE = ExampleVersion.majorMinor(VERSION);

private EngineDeckV2Example() {
}

Expand Down Expand Up @@ -261,7 +252,7 @@ private static DocumentNode heroScene() {
layers.add(at(heroLogo(), 42, 24));
layers.add(at(versionPill(), 742, 36));

layers.add(at(canvasText("HeroKicker", "GRAPHCOMPOSE " + VERSION_LINE + " / MODULE-FIRST",
layers.add(at(canvasText("HeroKicker", "GRAPHCOMPOSE / MODULE-FIRST",
darkEyebrow(), TextAlign.LEFT, 420), 52, 126));
layers.add(at(canvasText("HeroLine1", "Compose once.", heroTitle(), TextAlign.LEFT, 410), 52, 156));
layers.add(at(canvasText("HeroLine2", "Render through modules.", heroAccentTitle(), TextAlign.LEFT, 330), 52, 202));
Expand All @@ -271,7 +262,7 @@ private static DocumentNode heroScene() {
heroBody(), TextAlign.LEFT, 410), 54, 262));
layers.add(at(heroCoordinate(), 52, 333));

layers.add(at(canvasText("GraphLabel", "THE " + VERSION_LINE + " MODULE GRAPH",
layers.add(at(canvasText("GraphLabel", "THE MODULE GRAPH",
darkEyebrow(), TextAlign.LEFT, 690), 622, 112));
layers.addAll(heroModuleConnectors());
layers.add(at(heroCoreCard(), 646, 152));
Expand Down Expand Up @@ -627,7 +618,7 @@ private static DocumentNode moduleMatrix() {
double h = 180;
List<CanvasChild> nodes = new ArrayList<>();
nodes.add(at(roundedRect(w, h, 15, DARK_CARD, DARK_LINE), 0, 0));
nodes.add(at(canvasText("MatrixKicker", "THE LOCKSTEP 2.0 TRAIN",
nodes.add(at(canvasText("MatrixKicker", "THE LOCKSTEP TRAIN",
darkEyebrow(), TextAlign.LEFT, 610), 20, 17));
nodes.add(at(canvasText("MatrixBody",
"One version, smaller dependency trees, explicit format boundaries.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,9 @@ public static Path generate() throws Exception {
document.header(DocumentHeaderFooter.builder()
.zone(DocumentHeaderFooterZone.HEADER)
.leftText("GraphCompose · Feature catalog")
.rightText("{date}")
// No {date} here: the catalogue is committed as a preview, and the
// token would re-date it on every render. PdfChromeExample is where
// the token itself is demonstrated.
.fontSize(8f).textColor(MUTED)
.showSeparator(true)
.separatorColor(THEME.palette().rule())
Expand Down Expand Up @@ -436,7 +438,7 @@ public static Path generate() throws Exception {
feature(flow, "Page chrome — this document's own header, footer, outline", """
document.metadata(DocumentMetadata.builder().title("…").author("…").build());
document.header(DocumentHeaderFooter.builder().zone(HEADER)
.leftText("GraphCompose · Feature catalog").rightText("{date}")…);
.leftText("GraphCompose · Feature catalog")…);
document.footer(…centerText("Page {page} of {pages}")…);
paragraph.bookmark(new DocumentBookmarkOptions("Feature catalog", 0))""",
demo -> demo.addParagraph(p -> p
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import com.demcha.examples.support.theme.BusinessTheme;
import com.demcha.compose.font.FontName;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.ExampleVersion;
import com.demcha.examples.support.PerfBaseline;

import java.nio.file.Path;
Expand All @@ -42,12 +41,13 @@
* generate, not a feature checklist. Use it as a reference when
* composing your own multi-page documents.</p>
*
* <p>The narrative is deliberately undated. This file is regenerated and
* re-committed on every release, so anything that names a version, a date or a
* measurement would go stale between one release and the next — as it did. What
* survives is either sample copy that cannot age or a figure read at render time
* from {@link com.demcha.examples.support.PerfBaseline} and
* {@link com.demcha.examples.support.ExampleVersion}.</p>
* <p>Nothing here is read from the clock or from the reactor. This file is
* regenerated and re-committed on every release, and a version, a wall-clock date
* or a re-measured figure would make the committed copy differ from a fresh render
* for reasons no reader cares about — as they did. The header names the report's
* own quarter, the footer names no version, and the one live figure comes from
* {@link com.demcha.examples.support.PerfBaseline}, which is a committed
* measurement rather than a fresh one.</p>
*/
public final class MasterShowcaseExample {
private static final BusinessTheme THEME = BusinessTheme.modern();
Expand Down Expand Up @@ -109,7 +109,11 @@ static void compose(DocumentSession document) {
document.header(DocumentHeaderFooter.builder()
.zone(DocumentHeaderFooterZone.HEADER)
.leftText("GraphCompose · Master showcase")
.rightText("{date}")
// A fixed period rather than {date}: this document is committed as a
// README preview, and a header that re-dates itself on every render
// makes the committed copy differ from a fresh one for no reason a
// reader would care about. The report is a Q2 fiction anyway.
.rightText("Q2 2026")
.fontSize(9f)
.textColor(MUTED)
.showSeparator(true)
Expand All @@ -119,7 +123,7 @@ static void compose(DocumentSession document) {

document.footer(DocumentHeaderFooter.builder()
.zone(DocumentHeaderFooterZone.FOOTER)
.leftText("GraphCompose " + ExampleVersion.currentLine() + " — sample report")
.leftText("GraphCompose — sample report")
.rightText("Page {page} of {pages}")
.fontSize(9f)
.textColor(MUTED)
Expand Down Expand Up @@ -295,7 +299,7 @@ static void compose(DocumentSession document) {
"Initiative " + (i + 1) + " — quarterly delivery slice",
String.format("%d h", 40 + i * 3),
i % 3 == 0 ? "Studio" : i % 3 == 1 ? "Audit" : "Build",
String.format("£ %,d", 1_800 + i * 215));
String.format(Locale.ROOT, "£ %,d", 1_800 + i * 215));
}
table.totalRow(DocumentTableStyle.builder()
.padding(new DocumentInsets(8, 9, 8, 9))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.demcha.compose.document.svg.SvgIcon;
import com.demcha.compose.font.FontName;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.support.ExampleVersion;

import java.nio.file.Path;
import java.util.ArrayList;
Expand Down Expand Up @@ -286,7 +287,7 @@ private static DocumentNode coordinateCard() {
.margin(DocumentInsets.zero())
.build(), 22, -12, LayerAlign.CENTER_LEFT)
.position(new ParagraphBuilder()
.text("v2.1.0")
.text("v" + ExampleVersion.withoutQualifier())
.textStyle(monoBold(12.5, ORANGE))
.margin(DocumentInsets.zero())
.build(), 22, 13, LayerAlign.CENTER_LEFT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,43 @@
import java.util.regex.Pattern;

/**
* The reactor version, for examples that print it.
* The version examples print.
*
* <p>Read from the filtered {@code banner.properties} rather than written out,
* because example documents are regenerated on every release: a literal keeps
* announcing whichever line it was typed on. Shared so the value cannot drift
* between the documents that show it.</p>
*
* <p>The reactor is the default, not the only answer:
* {@link #DISPLAY_VERSION_PROPERTY} overrides it. Between releases the reactor
* sits on the next patch, so a render taken from a development branch names a
* version nobody can depend on yet — the override is how a published document
* gets reproduced from a branch that has moved past it.</p>
*/
public final class ExampleVersion {

private static final Pattern MAJOR_MINOR = Pattern.compile("^(\\d+)\\.(\\d+)");

/**
* What the override accepts: a released version, nothing else.
*
* <p>Rejecting rather than rewriting, because every render site runs the value
* through {@link #withoutQualifier()} — so a pre-release passed here would reach the
* page as the final version of that line, which is not published and may never be.
* The committed previews show what is on Maven Central; a caller asking for anything
* else is asking for a document that misrepresents the project.</p>
*/
private static final Pattern RELEASE_VERSION = Pattern.compile("\\d+\\.\\d+\\.\\d+");

private static final String CURRENT = load();

private ExampleVersion() {
}

/**
* The full reactor version, or {@code "dev"} when the resource is absent or
* unfiltered (running straight from sources).
* The full version to display — the override when one is set, otherwise the
* reactor's, or {@code "dev"} when the resource is absent or unfiltered
* (running straight from sources).
*
* @return version string
*/
Expand Down Expand Up @@ -72,6 +90,17 @@ public static String majorMinor(String version) {
return matcher.find() ? matcher.group(1) + "." + matcher.group(2) : version;
}

/**
* The version a render should display, when it is not the reactor's.
*
* <p>Between cuts the reactor sits on the next patch — {@code 2.1.1-SNAPSHOT} while
* {@code 2.1.0} is what people can depend on — so a render taken from {@code develop}
* names a version that does not exist yet. Stripping the qualifier is not enough:
* the patch number itself has moved. Anything that needs to reproduce a published
* document passes the published version here.</p>
*/
public static final String DISPLAY_VERSION_PROPERTY = "graphcompose.examples.displayVersion";

private static String load() {
Properties banner = new Properties();
try (InputStream in = ExampleVersion.class.getResourceAsStream("/banner.properties")) {
Expand All @@ -81,7 +110,44 @@ private static String load() {
} catch (IOException ignored) {
// Fall through to the development label below.
}
String value = banner.getProperty("version");
return value == null || value.isBlank() || value.startsWith("@") ? "dev" : value.trim();
return resolve(System.getProperty(DISPLAY_VERSION_PROPERTY), banner.getProperty("version"));
}

/**
* Chooses between the override and the filtered value.
*
* <p>Separate from {@link #load()} because the result is cached in a static
* field: once any code has touched this class the property can no longer change
* the answer, so a test that sets it proves nothing. The decision is testable;
* the caching is not, and does not need to be.</p>
*
* @param override the {@link #DISPLAY_VERSION_PROPERTY} value, may be null
* @param filtered the {@code version} entry from {@code banner.properties}
* @return the version to display
*/
static String resolve(String override, String filtered) {
// The override wins, and it is the only way to render a version other than
// the one the build carries. banner.properties keeps sourcing
// @project.version@ — a guard requires that — so the reactor stays the
// default answer.
if (override != null && !override.isBlank()) {
// A leading "v" is how a version is written in prose and how half the
// render sites print it. Accepting it and stripping it once keeps the two
// spellings from reaching the page as "vv2.1.0".
String requested = override.trim().replaceFirst("^[vV]", "");
if (!RELEASE_VERSION.matcher(requested).matches()) {
throw new IllegalArgumentException(
"-D" + DISPLAY_VERSION_PROPERTY + "=" + override + " is not a released"
+ " version. These documents are committed as previews of what is on"
+ " Maven Central, and every site that prints the value runs it through"
+ " withoutQualifier(), which would turn 2.2.0-rc.1 into 2.2.0 — a"
+ " version that does not exist. Pass the published one, or leave the"
+ " property unset to render the reactor's.");
}
return requested;
}
return filtered == null || filtered.isBlank() || filtered.startsWith("@")
? "dev"
: filtered.trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

/**
* Guards the version string the published example documents render.
Expand Down Expand Up @@ -35,4 +36,55 @@ void theLineIsTheLeadingMajorMinor() {
assertThat(ExampleVersion.majorMinor("2.1.1-SNAPSHOT")).isEqualTo("2.1");
assertThat(ExampleVersion.majorMinor("dev")).isEqualTo("dev");
}

/**
* The override decides what a committed document says, so what it accepts is worth
* pinning. Driven through {@code resolve} rather than the system property: the answer
* is cached in a static field, so a test that sets the property proves only that some
* earlier test had not touched the class yet.
*/
@Test
void aDisplayVersionOverridesTheReactorsAndIsNormalised() {
assertThat(ExampleVersion.resolve("2.1.0", "2.1.1-SNAPSHOT")).isEqualTo("2.1.0");
assertThat(ExampleVersion.resolve(" 2.1.0 ", "2.1.1-SNAPSHOT")).isEqualTo("2.1.0");
assertThat(ExampleVersion.resolve("v2.1.0", "2.1.1-SNAPSHOT"))
.describedAs("a leading v is how a version is written in prose, and one render "
+ "site prepends its own — accepting both spellings here is what keeps "
+ "vv2.1.0 off the page")
.isEqualTo("2.1.0");
}

/**
* A pre-release passed to the override is refused, not quietly promoted.
*
* <p>Every site that prints the value calls {@code withoutQualifier()}, so
* {@code 2.2.0-rc.1} would render as {@code 2.2.0} — the final version of that line,
* which is not on Maven Central and may never be. The release tooling really does
* cut {@code -rc} versions, so this is reachable rather than theoretical.</p>
*/
@Test
void aPreReleaseOverrideIsRefused() {
assertThatThrownBy(() -> ExampleVersion.resolve("2.2.0-rc.1", "2.1.1-SNAPSHOT"))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("2.2.0-rc.1")
.hasMessageContaining("does not exist");
assertThatThrownBy(() -> ExampleVersion.resolve("2.1.1-SNAPSHOT", "2.1.1-SNAPSHOT"))
.isInstanceOf(IllegalArgumentException.class);
assertThatThrownBy(() -> ExampleVersion.resolve("latest", "2.1.1-SNAPSHOT"))
.isInstanceOf(IllegalArgumentException.class);
}

@Test
void withoutAnOverrideTheReactorAnswers() {
assertThat(ExampleVersion.resolve(null, "2.1.1-SNAPSHOT")).isEqualTo("2.1.1-SNAPSHOT");
assertThat(ExampleVersion.resolve(" ", "2.1.1-SNAPSHOT"))
.describedAs("a blank override is an unset one, not an instruction to render "
+ "nothing")
.isEqualTo("2.1.1-SNAPSHOT");
assertThat(ExampleVersion.resolve(null, "@project.version@"))
.describedAs("an unfiltered resource means the module was run straight from "
+ "sources")
.isEqualTo("dev");
assertThat(ExampleVersion.resolve(null, null)).isEqualTo("dev");
}
}
Loading
Loading