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

### Documentation

- **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
one. Generation now clears the output tree first, inside the runner itself, so the
release script, CI and the tests all get the same guarantee instead of the one path
that happened to remember. `ShowcaseSync` removes the directories as well as the files
it replaces, which makes the published tree a function of the catalogue rather than a
record of everything ever generated.
- **Nine committed previews are gone and two examples now run.** Seven were pre-2.0
renders of CV presets whose current versions the runner already produced, so the
gallery linked the older snapshot of each; those links follow the current render and
the stale files are removed. Two more, an invoice and a proposal, were left by a rename
and nothing linked them at all. The cover-letter example and a custom-theme CV were
never wired into the runner, so one of them backed a link with a file no code produced
— both are generated now, and both have a showcase entry.
- **The template-authoring guide describes the packages that exist.** It told a
contributor to put a new family under `templates.<family>.v2` in exactly five
sub-packages, one of them a per-family `theme/` — a layout 2.0 replaced. A family has
Expand Down
Binary file modified assets/readme/examples/cover-letter.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed assets/readme/examples/cv-modern-professional.pdf
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed assets/readme/examples/invoice.pdf
Binary file not shown.
Binary file removed assets/readme/examples/proposal.pdf
Binary file not shown.
24 changes: 14 additions & 10 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ are with the canonical DSL, then jump to its detailed section below.

| Example | What it shows | Preview · Source |
|---|---|---|
| [CV — single template](#cv-single-template) | One CV via `ModernProfessional.create()` on a `CvDocument` | [PDF](../assets/readme/examples/cv-modern-professional.pdf) · [Source](src/main/java/com/demcha/examples/templates/cv/CvFileExample.java) |
| [CV — single template](#cv-single-template) | One CV via `ModernProfessional.create()` on a `CvDocument` | [PDF](../assets/readme/examples/cv-modern-professional-v2.pdf) · [Source](src/main/java/com/demcha/examples/templates/cv/v2/CvModernV2Example.java) |
| [Invoice — cinematic V2](#invoice-cinematic-v2) | `ModernInvoice + BrandTheme.invoiceModern()` — the recommended invoice path | [PDF](../assets/readme/examples/invoice-cinematic.pdf) · [Source](src/main/java/com/demcha/examples/templates/invoice/InvoiceCinematicFileExample.java) |
| [Cover Letter](#cover-letter) | One-page cover letter composed in the canonical DSL, section presets carrying the hierarchy | [PDF](../assets/readme/examples/cover-letter.pdf) · [Source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterFileExample.java) |
| [Module-first Profile](#module-first-profile) | Authoring directly against `DocumentSession.module(...).paragraph(...)` — DSL-direct, no template | [PDF](../assets/readme/examples/module-first-profile.pdf) · [Source](src/main/java/com/demcha/examples/flagships/ModuleFirstFileExample.java) |
Expand Down Expand Up @@ -205,8 +205,8 @@ One CV rendered through the layered template surface:
The preset is one final class with `create()` / `create(BrandTheme)`
factories — copy-and-tweak rather than fork-a-monolith.

[📄 View PDF](../assets/readme/examples/cv-modern-professional.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/templates/cv/CvFileExample.java)
[📄 View PDF](../assets/readme/examples/cv-modern-professional-v2.pdf) ·
[📜 Full source](src/main/java/com/demcha/examples/templates/cv/v2/CvModernV2Example.java)

### CV — template gallery

Expand All @@ -218,15 +218,19 @@ preset for your own CV product. Each preset is a one-liner factory

| Variant | PDF |
|---|---|
| Modern professional | [PDF](../assets/readme/examples/cv-modern-professional.pdf) |
| Nordic clean | [PDF](../assets/readme/examples/cv-nordic-clean.pdf) |
| 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 | [PDF](../assets/readme/examples/cv-engineering-resume.pdf) |
| Panel | [PDF](../assets/readme/examples/cv-panel.pdf) |
| Modern professional | [PDF](../assets/readme/examples/cv-modern-professional-v2.pdf) |
| Nordic clean | [PDF](../assets/readme/examples/cv-nordic-clean-v2.pdf) |
| Classic serif | [PDF](../assets/readme/examples/cv-classic-serif-v2.pdf) |
| Compact mono | [PDF](../assets/readme/examples/cv-compact-mono-v2.pdf) |
| Timeline minimal | [PDF](../assets/readme/examples/cv-timeline-minimal-v2.pdf) |
| Engineering resume | [PDF](../assets/readme/examples/cv-engineering-resume-v2.pdf) |
| Panel | [PDF](../assets/readme/examples/cv-panel-v2.pdf) |
| Executive · BoxedSections · CenteredHeadline · BlueBanner · EditorialBlue · SidebarPortrait · MonogramSidebar · MintEditorial · MinimalUnderlined | run the gallery to render |

The previews above come from the per-preset examples under
[`templates/cv/v2/`](src/main/java/com/demcha/examples/templates/cv/v2), which the
catalogue runner generates. The gallery below renders every preset from one entry point
instead, and is not part of that run — use it to produce the whole set locally:
[📜 Full source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java)

### Cover letter — template gallery
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import com.demcha.examples.features.title.BookTemplateExample;
import com.demcha.examples.features.title.PoetryTitlePageExample;
import com.demcha.examples.features.transforms.TransformsExample;
import com.demcha.examples.support.ExampleOutputPaths;
import com.demcha.examples.flagships.BusinessReportExample;
import com.demcha.examples.flagships.BusinessReportPptxExample;
import com.demcha.examples.flagships.EngineDeckExample;
Expand All @@ -58,6 +59,7 @@
import com.demcha.examples.flagships.MavenBannerPptxExample;
import com.demcha.examples.flagships.ModuleFirstFileExample;
import com.demcha.examples.flagships.TwinOutputExample;
import com.demcha.examples.templates.coverletter.CoverLetterFileExample;
import com.demcha.examples.templates.coverletter.CvBlueBannerLetterV2Example;
import com.demcha.examples.templates.coverletter.CvBoxedSectionsLetterV2Example;
import com.demcha.examples.templates.coverletter.CvCenteredHeadlineLetterV2Example;
Expand All @@ -82,6 +84,7 @@
import com.demcha.examples.templates.cv.v2.CvEngineeringResumeExample;
import com.demcha.examples.templates.cv.v2.CvExecutiveExample;
import com.demcha.examples.templates.cv.v2.CvMinimalUnderlinedExample;
import com.demcha.examples.templates.cv.v2.CvMintEditorialCustomExample;
import com.demcha.examples.templates.cv.v2.CvMintEditorialExample;
import com.demcha.examples.templates.cv.v2.CvModernV2Example;
import com.demcha.examples.templates.cv.v2.CvMonogramSidebarExample;
Expand Down Expand Up @@ -109,6 +112,11 @@ private GenerateAllExamples() {
}

public static void main(String[] args) throws Exception {
// The catalogue is a statement about the code, so it starts empty: an
// example renamed or deleted must not leave its old document behind for
// the showcase to publish and the guards to count.
ExampleOutputPaths.clean();

// === Templates ===
// CV / Resume (v2 layered — 16 presets, one example per preset)
System.out.println("Generated: " + CvBlueBannerExample.generate());
Expand All @@ -121,6 +129,8 @@ public static void main(String[] args) throws Exception {
System.out.println("Generated: " + CvExecutiveExample.generate());
System.out.println("Generated: " + CvMinimalUnderlinedExample.generate());
System.out.println("Generated: " + CvMintEditorialExample.generate());
System.out.println("Generated: " + CvMintEditorialCustomExample.generate());
System.out.println("Generated: " + CoverLetterFileExample.generate());
System.out.println("Generated: " + CvModernV2Example.generate());
System.out.println("Generated: " + CvMonogramSidebarExample.generate());
System.out.println("Generated: " + CvNordicCleanExample.generate());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,70 @@
package com.demcha.examples.support;

import java.io.IOException;
import java.nio.file.AccessDeniedException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Comparator;
import java.util.stream.Stream;

public final class ExampleOutputPaths {
private ExampleOutputPaths() {
}

/** The tree every example writes into. */
public static Path root() {
return baseDirectory().resolve("target").resolve("generated-pdfs")
.toAbsolutePath().normalize();
}

/**
* Empties the output tree so what remains is exactly what this run produced.
*
* <p>The examples only write. Rename an example's output or delete the example and
* its old file stays behind, indistinguishable from a current one — the showcase
* then publishes a document nothing produces, and the guards that read the tree
* count it as evidence. Clearing first is what makes the tree a statement about the
* code rather than about the order in which commands were run.</p>
*
* @return the emptied root
*/
public static Path clean() throws IOException {
Path root = root();
if (Files.isDirectory(root)) {
try (Stream<Path> paths = Files.walk(root)) {
for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
if (!path.equals(root)) {
delete(path);
}
}
}
}
Files.createDirectories(root);
return root;
}

/**
* Deletes one entry, translating the failure a reader can actually act on.
*
* <p>A document held open in a viewer is the way this fails on Windows, and it is
* common enough that the runbook lists it. The raw {@code AccessDeniedException}
* arrives with a path and nothing else, at the very start of a run that has produced
* nothing yet, which reads like the generator is broken rather than like a window
* needs closing.</p>
*/
private static void delete(Path path) throws IOException {
try {
Files.delete(path);
} catch (AccessDeniedException blocked) {
throw new IOException("Cannot clear " + path + " before regenerating the examples."
+ " Something is holding the file open — a PDF viewer is the usual answer on"
+ " Windows. Close it and run again.", blocked);
}
}

public static Path prepare(String fileName) throws Exception {
Path root = baseDirectory().resolve("target").resolve("generated-pdfs");
Path root = root();
Files.createDirectories(root);
return root.resolve(fileName).toAbsolutePath().normalize();
}
Expand All @@ -26,7 +81,7 @@ public static Path prepare(String fileName) throws Exception {
* @return absolute path under the categorised generated-pdfs tree
*/
public static Path prepare(String category, String fileName) throws Exception {
Path root = baseDirectory().resolve("target").resolve("generated-pdfs").resolve(category);
Path root = root().resolve(category);
Files.createDirectories(root);
return root.resolve(fileName).toAbsolutePath().normalize();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,16 @@ record Entry(String title, String description, List<String> tags, String codeUrl
cv("cv-monogram-sidebar-v2", "CvMonogramSidebarExample", "Monogram Sidebar", "Sidebar with monogram badge, accent rule, and structured contact + skills column.", "sidebar", "monogram");
cv("cv-minimal-underlined-v2", "CvMinimalUnderlinedExample", "Minimal Underlined", "Single-column layout with underlined section titles and tight whitespace — minimalist reference shape.", "minimal");
cv("cv-mint-editorial-v2", "CvMintEditorialExample", "Mint Editorial", "Magazine-style editorial CV with mint accent palette and two-column body.", "editorial", "mint");
cv("cv-mint-editorial-v2-custom", "CvMintEditorialCustomExample", "Mint Editorial (custom band)", "The same preset with one colour changed through its Options — a kraft-paper masthead band, everything else left at the preset's defaults.", "editorial", "mint");

// ===== Templates / Cover Letter (v2 layered, paired 1:1 with CV) =====
// Registered directly: letter() points at the layered preset examples under
// coverletter/v2, and this one sits a level up because it composes without a
// preset at all.
ENTRIES.put("cover-letter", entry("Cover Letter",
"One page composed straight in the canonical DSL — section presets carry the hierarchy, no template involved.",
withCategory("letter"),
EX_BASE + "/templates/coverletter/CoverLetterFileExample.java"));
letter("cover-letter-modern-professional-v2", "CvModernProfessionalLetterV2Example", "Modern Professional letter", "Letter paired with the Modern Professional CV palette.");
letter("cover-letter-nordic-clean-v2", "CvNordicCleanLetterV2Example", "Nordic Clean letter", "Letter paired with the Nordic Clean CV palette.");
letter("cover-letter-classic-serif-v2", "CvClassicSerifLetterV2Example", "Classic Serif letter", "Letter with Times-style serif typography.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
Expand Down Expand Up @@ -89,6 +92,7 @@ public static void main(String[] args) throws Exception {
+ ". Run GenerateAllExamples first — refusing to clear the published"
+ " showcase and write an empty manifest.");
}
requireCompleteCatalogue(pdfs, generatedPdfs);

// The copy below replaces files but never removes them, so an example that was
// renamed or deleted kept its artifact published on GitHub Pages indefinitely —
Expand Down Expand Up @@ -189,21 +193,74 @@ private static void renderPreview(Path pdfPath, Path pngTarget) throws IOExcepti
}

/**
* Removes every published file under a subtree, leaving the directories in place.
* Only files matter: git tracks no empty directory, and the copy below recreates
* whatever it needs. Deleting the directories too is what makes this fragile on
* Windows, where a just-closed handle can still fail the parent's removal with
* {@code AccessDeniedException}. A missing tree is a no-op — the normal case on a
* Refuses to publish a catalogue that is missing documents the register describes.
*
* <p>"Not empty" stopped being enough the moment generation began by clearing the
* tree. A run that dies partway now leaves exactly the documents written before the
* throw, and that passes an emptiness check — so the sync would mirror the remains,
* delete every published file the run never got to, and rewrite the manifest to
* match. The site ends up internally consistent and quietly missing half its
* examples, which is worse than an obvious failure.</p>
*
* <p>The register is the yardstick because it is the one list that says what the
* catalogue is meant to contain. Deleting an example legitimately shrinks the
* catalogue, and that stays possible — the entry goes with it, which the coverage
* guard requires anyway.</p>
*/
private static void requireCompleteCatalogue(List<Path> pdfs, Path generatedPdfs) {
Set<String> produced = pdfs.stream()
.map(pdf -> pdf.getFileName().toString())
.map(name -> name.substring(0, name.length() - ".pdf".length()))
.collect(Collectors.toCollection(TreeSet::new));

Set<String> missing = new TreeSet<>(ShowcaseMetadata.registeredEntries().keySet());
missing.removeAll(produced);
if (!missing.isEmpty()) {
throw new IllegalStateException(
"The generated catalogue is missing " + missing.size() + " document(s) the"
+ " showcase register describes: " + missing + ". Generation starts by"
+ " emptying " + generatedPdfs + ", so a run that failed partway leaves"
+ " exactly what it managed to write. Re-run GenerateAllExamples and let it"
+ " finish — publishing now would delete the missing documents from the site"
+ " and rewrite the manifest without them. If an example was removed on"
+ " purpose, remove its register entry too.");
}
}

/**
* Empties a published subtree. A missing tree is a no-op — the normal case on a
* fresh checkout.
*
* <p>Every file goes, and a failure there stops the sync: a published file that
* survives is a document the site still serves and the manifest no longer lists.</p>
*
* <p>Directories are removed too, deepest first, but only where the filesystem
* allows. On Windows a just-closed handle can still fail the parent's removal with
* {@code AccessDeniedException} — observed here on
* {@code web/showcase/pdf/templates/schedule} — and an empty folder left behind is
* a cosmetic blemish on the deployed tree, invisible to git and to every reader who
* arrives through the manifest. Failing the whole publish over one would trade a
* real problem for a tidiness preference.</p>
*/
private static void deletePublishedFiles(Path root) throws IOException {
if (!Files.exists(root)) {
return;
}
try (Stream<Path> walk = Files.walk(root)) {
List<Path> files = walk.filter(Files::isRegularFile).toList();
for (Path file : files) {
Files.delete(file);
for (Path path : walk.sorted(Comparator.reverseOrder()).toList()) {
if (path.equals(root)) {
continue;
}
if (Files.isDirectory(path)) {
try {
Files.delete(path);
} catch (IOException locked) {
System.out.println("Left in place (the filesystem would not remove it): "
+ path + " — " + locked.getClass().getSimpleName());
}
} else {
Files.delete(path);
}
}
}
}
Expand Down
Loading
Loading