diff --git a/.gitignore b/.gitignore index 80f2e7668..0c80806ab 100644 --- a/.gitignore +++ b/.gitignore @@ -45,14 +45,6 @@ render-pdf/logs/ *.pdf # Allow PDF previews that are committed README assets. !assets/readme/**/*.pdf -# Allow PDF previews that ship with the GitHub Pages showcase site. -!docs/assets/**/*.pdf -!docs/showcase/**/*.pdf -# Same whitelist for the Next.js showcase site under site/ — 53 PDFs -# mirrored from docs/showcase by Sync-SiteShowcase, plus 3 Playground -# preview PDFs (hello/invoice/cv) bundled with site/public/previews/. -!site/public/showcase/**/*.pdf -!site/public/previews/*.pdf # Same whitelist for the live GitHub Pages showcase under web/. The showcase # moved out of docs/ into web/ but this list was not updated, so every newly # added example PDF was silently ignored and 404'd on the deployed site. diff --git a/CHANGELOG.md b/CHANGELOG.md index f54c90bf8..94bb520fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,23 @@ follow semantic versioning; release dates are ISO 8601. - **`graph-compose-render-pptx` declares PDFBox.** It compiles against `org.apache.pdfbox` types while declaring only `fontbox`, taking the rest transitively; the resolved version is unchanged. +- **The release publishes the showcase it just built.** `cut-release.ps1` never + ran `GenerateAllExamples`, so the site was synced from whatever happened to be + in `examples/target/generated-pdfs` — nothing at all on a clean checkout, which + aborted the cut. Step 4 regenerates the catalogue first. +- **A removed example stops being published.** `ShowcaseSync` copied but never + deleted, so an artifact whose example was renamed or removed stayed live on the + site indefinitely, reachable by URL and absent from the manifest. Three such + documents are gone. +- **Every generated document has a showcase card.** Eight artifacts had no + metadata entry and published as filename-derived placeholders linking the + examples root — among them both flagships the 2.1 line exists to show. + `EngineDeckV2Example`, which renders the deck the README banner is cut from, + was never wired into the example runner at all. +- **Five committed previews were re-rendered.** The largest, the PDF-chrome + preview, was drawn at low alpha throughout with the watermark bleeding through + every panel. Eleven committed binaries that nothing references, 9 MiB of them, + are removed. - **The release script owns the README release-status block.** It was a hand-edit the script only validated, and the validation demanded the *target* version — so between releases `develop` had to advertise an unpublished version diff --git a/assets/GraphComposeCover.png b/assets/GraphComposeCover.png deleted file mode 100644 index a4f9fce6d..000000000 Binary files a/assets/GraphComposeCover.png and /dev/null differ diff --git a/assets/Overview.png b/assets/Overview.png deleted file mode 100644 index be7e241c4..000000000 Binary files a/assets/Overview.png and /dev/null differ diff --git a/assets/readme/GraphComposeLogo_final.svg b/assets/readme/GraphComposeLogo_final.svg deleted file mode 100644 index 2611e0501..000000000 --- a/assets/readme/GraphComposeLogo_final.svg +++ /dev/null @@ -1,60 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/assets/readme/compose-first-invoice-template.png b/assets/readme/compose-first-invoice-template.png deleted file mode 100644 index f947380e8..000000000 Binary files a/assets/readme/compose-first-invoice-template.png and /dev/null differ diff --git a/assets/readme/cv-layout-debug.png b/assets/readme/cv-layout-debug.png deleted file mode 100644 index 56f3cac1f..000000000 Binary files a/assets/readme/cv-layout-debug.png and /dev/null differ diff --git a/assets/readme/cv-preview-clean.png b/assets/readme/cv-preview-clean.png deleted file mode 100644 index 04ebb01de..000000000 Binary files a/assets/readme/cv-preview-clean.png and /dev/null differ diff --git a/assets/readme/examples/pdf-chrome.pdf b/assets/readme/examples/pdf-chrome.pdf index f130ba587..d3f332da1 100644 Binary files a/assets/readme/examples/pdf-chrome.pdf and b/assets/readme/examples/pdf-chrome.pdf differ diff --git a/assets/readme/examples/project-proposal-cinematic.pdf b/assets/readme/examples/project-proposal-cinematic.pdf index 73211bd29..a8393be78 100644 Binary files a/assets/readme/examples/project-proposal-cinematic.pdf and b/assets/readme/examples/project-proposal-cinematic.pdf differ diff --git a/assets/readme/examples/proposal-cinematic.pdf b/assets/readme/examples/proposal-cinematic.pdf index 8ba569171..f948c0143 100644 Binary files a/assets/readme/examples/proposal-cinematic.pdf and b/assets/readme/examples/proposal-cinematic.pdf differ diff --git a/assets/readme/examples/section-presets.pdf b/assets/readme/examples/section-presets.pdf index d26e5ddba..ed0006af9 100644 Binary files a/assets/readme/examples/section-presets.pdf and b/assets/readme/examples/section-presets.pdf differ diff --git a/assets/readme/examples/shape-container.pdf b/assets/readme/examples/shape-container.pdf index 6b3868ae8..f10805ec2 100644 Binary files a/assets/readme/examples/shape-container.pdf and b/assets/readme/examples/shape-container.pdf differ diff --git a/assets/readme/fonts-preview.png b/assets/readme/fonts-preview.png deleted file mode 100644 index fcae469ee..000000000 Binary files a/assets/readme/fonts-preview.png and /dev/null differ diff --git a/assets/readme/repository_showcase_render_guides.png b/assets/readme/repository_showcase_render_guides.png deleted file mode 100644 index 0b6db35f4..000000000 Binary files a/assets/readme/repository_showcase_render_guides.png and /dev/null differ diff --git a/assets/readme/v1.5/invoice-cinematic-v2.pdf b/assets/readme/v1.5/invoice-cinematic-v2.pdf deleted file mode 100644 index d862786be..000000000 Binary files a/assets/readme/v1.5/invoice-cinematic-v2.pdf and /dev/null differ diff --git a/assets/readme/v1.5/invoice-custom-theme-studio-emerald.pdf b/assets/readme/v1.5/invoice-custom-theme-studio-emerald.pdf deleted file mode 100644 index 3e006a416..000000000 Binary files a/assets/readme/v1.5/invoice-custom-theme-studio-emerald.pdf and /dev/null differ diff --git a/assets/readme/v1.5/proposal-cinematic-v2.pdf b/assets/readme/v1.5/proposal-cinematic-v2.pdf deleted file mode 100644 index 4ebd0155a..000000000 Binary files a/assets/readme/v1.5/proposal-cinematic-v2.pdf and /dev/null differ diff --git a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java index 447da85c9..b31065cb1 100644 --- a/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java +++ b/examples/src/main/java/com/demcha/examples/GenerateAllExamples.java @@ -46,6 +46,7 @@ import com.demcha.examples.flagships.BusinessReportExample; import com.demcha.examples.flagships.BusinessReportPptxExample; import com.demcha.examples.flagships.EngineDeckExample; +import com.demcha.examples.flagships.EngineDeckV2Example; import com.demcha.examples.flagships.EngineDeckPptxExample; import com.demcha.examples.flagships.FeatureCatalogExample; import com.demcha.examples.flagships.FinancialReportExample; @@ -222,7 +223,12 @@ public static void main(String[] args) throws Exception { System.out.println("Generated: " + BusinessReportExample.generate()); System.out.println("Generated: " + BusinessReportPptxExample.generate()); System.out.println("Generated: " + EngineDeckExample.generate()); + System.out.println("Generated: " + EngineDeckV2Example.generate()); System.out.println("Generated: " + EngineDeckPptxExample.generate()); + // The PDF first: ShowcaseSync builds a card per PDF and publishes a PPTX only + // as the same-named companion beside one, so a deck without its PDF twin never + // reaches the site at all. + System.out.println("Generated: " + MavenBannerPptxExample.generatePdf()); System.out.println("Generated: " + MavenBannerPptxExample.generate()); System.out.println("Generated: " + SocialCardExample.generate()); System.out.println("Generated: " + SocialCardExample.generatePptx()); diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java index b99afad35..6d9b8f906 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -78,11 +78,13 @@ record Entry(String title, String description, List tags, String codeUrl letter("cover-letter-mint-editorial-v2", "CvMintEditorialLetterV2Example", "Mint Editorial letter", "Letter paired with Mint Editorial CV — magazine-style mint accent."); // ===== Templates / Invoice ===== - invoice("invoice-cinematic", "Cinematic Invoice", "Layered ModernInvoice preset with theme-driven layout, advanced tables, and totals.", "invoice", "cinematic"); + invoice("invoice-cinematic", "InvoiceCinematicFileExample", "Cinematic Invoice", "Layered ModernInvoice preset with theme-driven layout, advanced tables, and totals.", "invoice", "cinematic"); + invoice("invoice-modern-v2", "v2/ModernInvoiceV2Example", "Modern Invoice", "The ModernInvoice preset composed straight from an InvoiceDocumentSpec — line items, totals and payment block driven by the BrandTheme rather than per-document styling.", "invoice"); // ===== Templates / Proposal ===== - proposal("proposal-cinematic", "Cinematic Proposal", "Layered ModernProposal layout with cover panel, hero spread, and rich typography.", "proposal", "cinematic"); - proposal("project-proposal-cinematic", "Project Proposal (cinematic)", "End-to-end project proposal with mountain hero, scope panels, and pricing summary.", "proposal", "cinematic"); + proposal("proposal-cinematic", "ProposalCinematicFileExample", "Cinematic Proposal", "Layered ModernProposal layout with cover panel, hero spread, and rich typography.", "proposal", "cinematic"); + proposal("project-proposal-cinematic", "CinematicProposalFileExample", "Project Proposal (cinematic)", "End-to-end project proposal with mountain hero, scope panels, and pricing summary.", "proposal", "cinematic"); + proposal("proposal-modern-v2", "v2/ModernProposalV2Example", "Modern Proposal", "The ModernProposal preset composed straight from its document spec — cover, scope sections and pricing table themed through BrandTheme.", "proposal"); // ===== Templates / Schedule ===== schedule("weekly-schedule", "Weekly Schedule", "Multi-day weekly schedule with shift assignments, category fills, and repeated header.", "schedule", "table"); @@ -91,6 +93,7 @@ record Entry(String title, String description, List tags, String codeUrl feature("lists", "nested-list-showcase", "NestedListExample", "Nested Lists", "ListBuilder.addItem(label, Consumer) — depth cascade, per-depth markers, mixed flat / nested authoring.", "lists", "v1.6"); feature("tables", "table-advanced", "TableAdvancedExample", "Advanced Tables", "Row span, column span, zebra rows, total rows, and repeating headers across page breaks.", "tables", "pagination"); feature("tables", "composed-table-cell-showcase", "ComposedTableCellExample", "Composed Table Cells", "DocumentTableCell.node(DocumentNode) — paragraphs, lists, sub-tables inside cells with two-pass measurement.", "tables", "v1.6"); + feature("tables", "inline-code-column-wrap", "InlineCodeColumnWrapExample", "Inline Code in Narrow Columns", "Inline-code chips inside a fixed-width column — how a token too wide for its cell wraps instead of overflowing the column edge.", "tables", "text"); feature("canvas", "canvas-layer-showcase", "CanvasLayerExample", "Canvas Layer (free-canvas)", "CanvasLayerNode — pixel-precise (x,y) placement of children inside a fixed bounding box.", "canvas", "v1.6", "absolute"); feature("shapes", "shape-container", "ShapeContainerExample", "Shape-as-Container", "Rounded rect, ellipse, circle containers with ClipPolicy and layered children.", "shapes", "clip"); feature("svg", "svg-icon-gallery", "SvgIconGalleryExample", "SVG Icon Gallery", "34 real-world multicolour svgrepo icons through SvgIcon.parse — native vector layers, the whole set 156 KB of sources.", "svg", "icons", "v1.8"); @@ -130,6 +133,7 @@ record Entry(String title, String description, List tags, String codeUrl feature("navigation", "page-reference", "PageReferenceExample", "Page References", "addPageReference(anchor, style, align) — prints the resolved page an anchor(...) lands on for \"see page N\" cross-refs, in one authoring pass via a second layout pass.", "navigation", "v1.9", "structure"); feature("navigation", "table-of-contents", "TocExample", "Table of Contents", "page.addTableOfContents(...) with entry(label, anchor) — clickable rows, a DocumentLeader.DOTS leader, and page numbers resolved automatically from the laid-out document.", "navigation", "structure", "v1.9"); feature("structure", "multi-section-document", "MultiSectionExample", "Multi-Section Document", "GraphCompose.documents(out).section(cover).section(body) — concatenate independent DocumentSessions, each with its own page size and chrome, into one PDF with no external merge.", "structure", "v1.9"); + feature("title", "poetry-title", "PoetryTitlePageExample", "Poetry Title Page", "A centred title page with generous vertical rhythm — margins, alignment and spacing carrying the composition instead of decoration.", "title"); feature("title", "book-template", "BookTemplateExample", "Book Template", "A book-style title page and chapters fronted by a clickable addTableOfContents(...); toc.entry(label, anchor) rows resolve each chapter's page in one pass with DocumentLeader.DOTS leaders.", "book", "toc", "v1.9"); // ===== Flagships ===== @@ -137,8 +141,12 @@ record Entry(String title, String description, List tags, String codeUrl flagship("business-report", "BusinessReportExample", "Business Report Cover", "Flagship cover page with hero panel, KPI table, and accent strip — ready-to-ship template.", "showcase", "cover"); flagship("module-first-profile", "ModuleFirstFileExample", "Module-First Authoring", "Authoring style focused on declaring data modules first, layout second.", "authoring"); flagship("twin-output", "TwinOutputExample", "Twin Output", "One 16:9 page written once and emitted twice from the same session — a print-ready PDF and a PowerPoint slide with identical geometry where text, panels, and vectors stay native, editable shapes.", "showcase", "flagship"); + flagship("engine-deck-v2", "EngineDeckV2Example", "Engine Deck — Module First", "The landscape deck the README banner is cut from: the 2.0 module graph, native vector charts, and comparative benchmark figures read from the committed snapshot at render time.", "showcase", "flagship"); flagship("engine-deck", "EngineDeckExample", "Engine Deck", "Landscape flagship deck — hero banner, SVG-icon feature spreads, and benchmark tables and charts the engine renders from comparative data.", "showcase", "flagship"); flagship("feature-catalog", "FeatureCatalogExample", "Feature Catalog", "A guided catalog of the engine's primitives, one section per capability, every heading registered as a PDF outline bookmark for a navigable index.", "showcase", "flagship"); + flagship("social-card", "SocialCardExample", "Social Preview Card", "The repository's 1280x640 social preview, itself a GraphCompose document — one sheet resolving into a portrait page and a 16:9 slide from the same content, so the card cannot drift from the palette and wordmark it is drawn with.", "showcase", "flagship"); + flagship("linkedin-carousel", "LinkedInCarouselExample", "LinkedIn Carousel", "A six-slide 4:5 carousel sized for a LinkedIn document post, typeset for a phone. Every figure is read at render time — the version from the filtered properties, the timings from the committed benchmark snapshot.", "showcase", "flagship"); + flagship("maven-banner", "MavenBannerPptxExample", "Maven Central Banner", "A single-slide brand banner emitted through the PPTX backend — gradient, rounded panels, native paths and text frames arriving in PowerPoint as an editable copy of the rendered page.", "showcase", "flagship", "pptx"); flagship("financial-report", "FinancialReportExample", "Financial Report", "A polished financial-report flagship — clipped-photo masthead, KPI tables, and vector charts combining the engine's data-viz and shape primitives.", "showcase", "flagship"); } @@ -213,16 +221,14 @@ private static void letter(String id, String exampleClass, String title, String EX_BASE + "/templates/coverletter/v2/" + exampleClass + ".java")); } - private static void invoice(String id, String title, String desc, String... tags) { + private static void invoice(String id, String exampleClass, String title, String desc, String... tags) { ENTRIES.put(id, entry(title, desc, withCategory("invoice", tags), - EX_BASE + "/templates/invoice/InvoiceCinematicFileExample.java")); + EX_BASE + "/templates/invoice/" + exampleClass + ".java")); } - private static void proposal(String id, String title, String desc, String... tags) { - String file = id.equals("project-proposal-cinematic") - ? "CinematicProposalFileExample" : "ProposalCinematicFileExample"; + private static void proposal(String id, String exampleClass, String title, String desc, String... tags) { ENTRIES.put(id, entry(title, desc, withCategory("proposal", tags), - EX_BASE + "/templates/proposal/" + file + ".java")); + EX_BASE + "/templates/proposal/" + exampleClass + ".java")); } private static void schedule(String id, String title, String desc, String... tags) { diff --git a/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java b/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java index ad0310c1a..a31ff462c 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java @@ -73,77 +73,97 @@ public static void main(String[] args) throws Exception { + ". Run GenerateAllExamples first."); } + // Collect BEFORE clearing. The directory existing is not evidence that anything + // was produced: a generation run that succeeds while writing nothing would + // otherwise delete the whole published site and replace the manifest with an + // empty one, and the failure would only be visible once the site was live. + List pdfs; + try (Stream walk = Files.walk(generatedPdfs)) { + pdfs = walk + .filter(p -> p.toString().endsWith(".pdf")) + .sorted() + .toList(); + } + if (pdfs.isEmpty()) { + throw new IllegalStateException("No PDFs under " + generatedPdfs + + ". Run GenerateAllExamples first — refusing to clear the published" + + " showcase and write an empty manifest."); + } + + // 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 — + // reachable by URL, absent from the manifest, and rendered from source that no + // longer exists. Clearing the three output trees first makes the published site + // a pure function of what GenerateAllExamples just produced. + for (String subtree : new String[] {"pdf", "pptx", "screenshots"}) { + deletePublishedFiles(showcaseRoot.resolve(subtree)); + } + // category → group → list of entries Map>> tree = new TreeMap<>(); int copied = 0; int rendered = 0; int decks = 0; - try (Stream walk = Files.walk(generatedPdfs)) { - List pdfs = walk - .filter(p -> p.toString().endsWith(".pdf")) - .sorted() - .toList(); - for (Path pdf : pdfs) { - Path rel = generatedPdfs.relativize(pdf); - String[] parts = rel.toString().replace('\\', '/').split("/"); - if (parts.length < 2) { - continue; - } - String category; - String group; - String fileName = parts[parts.length - 1]; - String basename = fileName.substring(0, fileName.length() - 4); - if (parts.length == 2) { - category = parts[0]; - group = "default"; - } else { - category = parts[0]; - group = parts[1]; - } - - Path pdfTarget = showcaseRoot.resolve("pdf").resolve(category).resolve(group).resolve(fileName); - Path pngTarget = showcaseRoot.resolve("screenshots").resolve(category).resolve(group) - .resolve(basename + ".png"); - Files.createDirectories(pdfTarget.getParent()); - Files.createDirectories(pngTarget.getParent()); + for (Path pdf : pdfs) { + Path rel = generatedPdfs.relativize(pdf); + String[] parts = rel.toString().replace('\\', '/').split("/"); + if (parts.length < 2) { + continue; + } + String category; + String group; + String fileName = parts[parts.length - 1]; + String basename = fileName.substring(0, fileName.length() - 4); + if (parts.length == 2) { + category = parts[0]; + group = "default"; + } else { + category = parts[0]; + group = parts[1]; + } - Files.copy(pdf, pdfTarget, StandardCopyOption.REPLACE_EXISTING); - copied++; - renderPreview(pdf, pngTarget); - rendered++; + Path pdfTarget = showcaseRoot.resolve("pdf").resolve(category).resolve(group).resolve(fileName); + Path pngTarget = showcaseRoot.resolve("screenshots").resolve(category).resolve(group) + .resolve(basename + ".png"); + Files.createDirectories(pdfTarget.getParent()); + Files.createDirectories(pngTarget.getParent()); - // A twin flagship renders the same composition to a deck beside its - // PDF. The deck is published as a second download on the same card: - // it shares the PDF's preview by construction — both backends draw - // the same resolved layout — and POI has no page rasterizer to make - // a separate one with. - String pptxUrl = null; - Path pptxSource = pdf.resolveSibling(basename + ".pptx"); - if (Files.isRegularFile(pptxSource)) { - Path pptxTarget = showcaseRoot.resolve("pptx").resolve(category).resolve(group) - .resolve(basename + ".pptx"); - Files.createDirectories(pptxTarget.getParent()); - Files.copy(pptxSource, pptxTarget, StandardCopyOption.REPLACE_EXISTING); - copied++; - decks++; - pptxUrl = relativeUrl(showcaseRoot, pptxTarget, siteRoot); - } + Files.copy(pdf, pdfTarget, StandardCopyOption.REPLACE_EXISTING); + copied++; + renderPreview(pdf, pngTarget); + rendered++; - ShowcaseMetadata.Entry meta = ShowcaseMetadata.lookup(basename, category, group); - ManifestEntry entry = new ManifestEntry( - basename, - meta.title(), - meta.description(), - meta.tags(), - relativeUrl(showcaseRoot, pdfTarget, siteRoot), - pptxUrl, - relativeUrl(showcaseRoot, pngTarget, siteRoot), - meta.codeUrl()); - tree.computeIfAbsent(category, c -> new TreeMap<>()) - .computeIfAbsent(group, g -> new ArrayList<>()) - .add(entry); + // A twin flagship renders the same composition to a deck beside its + // PDF. The deck is published as a second download on the same card: + // it shares the PDF's preview by construction — both backends draw + // the same resolved layout — and POI has no page rasterizer to make + // a separate one with. + String pptxUrl = null; + Path pptxSource = pdf.resolveSibling(basename + ".pptx"); + if (Files.isRegularFile(pptxSource)) { + Path pptxTarget = showcaseRoot.resolve("pptx").resolve(category).resolve(group) + .resolve(basename + ".pptx"); + Files.createDirectories(pptxTarget.getParent()); + Files.copy(pptxSource, pptxTarget, StandardCopyOption.REPLACE_EXISTING); + copied++; + decks++; + pptxUrl = relativeUrl(showcaseRoot, pptxTarget, siteRoot); } + + ShowcaseMetadata.Entry meta = ShowcaseMetadata.lookup(basename, category, group); + ManifestEntry entry = new ManifestEntry( + basename, + meta.title(), + meta.description(), + meta.tags(), + relativeUrl(showcaseRoot, pdfTarget, siteRoot), + pptxUrl, + relativeUrl(showcaseRoot, pngTarget, siteRoot), + meta.codeUrl()); + tree.computeIfAbsent(category, c -> new TreeMap<>()) + .computeIfAbsent(group, g -> new ArrayList<>()) + .add(entry); } for (Map> groups : tree.values()) { @@ -168,6 +188,26 @@ 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 + * fresh checkout. + */ + private static void deletePublishedFiles(Path root) throws IOException { + if (!Files.exists(root)) { + return; + } + try (Stream walk = Files.walk(root)) { + List files = walk.filter(Files::isRegularFile).toList(); + for (Path file : files) { + Files.delete(file); + } + } + } + private static String relativeUrl(Path showcaseRoot, Path target, Path docsRoot) { Path rel = docsRoot.relativize(target); return rel.toString().replace('\\', '/'); diff --git a/examples/src/test/java/com/demcha/examples/GenerateAllExamplesSmokeTest.java b/examples/src/test/java/com/demcha/examples/GenerateAllExamplesSmokeTest.java index 3ed0cb428..2832f9588 100644 --- a/examples/src/test/java/com/demcha/examples/GenerateAllExamplesSmokeTest.java +++ b/examples/src/test/java/com/demcha/examples/GenerateAllExamplesSmokeTest.java @@ -2,38 +2,98 @@ import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * CI-coverage smoke test: runs {@link GenerateAllExamples#main(String[])} - * end-to-end and verifies it completes without exceptions. + * end-to-end and then asserts against what it actually wrote. * *

Each individual example invokes the full canonical pipeline * ({@code DocumentSession} -> {@code DocumentDsl} -> layout compile -> - * PDF render) and writes a PDF to {@code examples/target/generated-pdfs}. + * render) and writes its output to {@code examples/target/generated-pdfs}. * If any example breaks at runtime, this test fails — closing the * audit's L4 gap that flagged {@code GenerateAllExamples} as never * exercised by CI.

* + *

Completing without throwing is necessary but not sufficient, so the + * assertions below read the output tree rather than the exit path.

+ * *

Lives under the sibling {@code examples/} Maven module so the * smoke runner's JUnit dependency does not pollute the main project's * test classpath.

*/ class GenerateAllExamplesSmokeTest { - @Test - void generateAllExamplesCompletesWithoutErrors() throws Exception { - // The main method prints "Generated: " lines for each - // example. We assert that it returns normally — any - // RuntimeException, IOException, or template wiring break - // surfaces here as a CI failure. + private static final Path GENERATED_ROOT = Path.of("target", "generated-pdfs"); + + @BeforeAll + static void generateEveryExample() throws Exception { GenerateAllExamples.main(new String[0]); + } + + @Test + void generateAllExamplesWritesPdfs() { + assertThat(GENERATED_ROOT) + .describedAs("GenerateAllExamples must write its output under %s", GENERATED_ROOT) + .isDirectoryRecursivelyContaining("glob:**/*.pdf"); + } + + /** + * Every generated deck needs a PDF of the same name beside it. + * + *

{@code ShowcaseSync} builds one card per PDF and copies a {@code .pptx} only + * as the same-named companion of one. A deck generated without its PDF twin is + * therefore invisible to the published site: it lands in {@code target}, never + * reaches {@code web/showcase}, and its hand-written {@code ShowcaseMetadata} entry + * goes silently unused. That is exactly what happened to the Maven banner, whose + * example exposes both {@code generate()} and {@code generatePdf()} while only the + * first was wired into the runner.

+ */ + @Test + void everyGeneratedDeckHasAPdfTwin() throws IOException { + Set orphanDecks = new TreeSet<>(); + try (Stream walk = Files.walk(GENERATED_ROOT)) { + List decks = walk + .filter(path -> path.toString().endsWith(".pptx")) + .toList(); + for (Path deck : decks) { + String fileName = deck.getFileName().toString(); + String stem = fileName.substring(0, fileName.length() - ".pptx".length()); + if (!Files.isRegularFile(deck.resolveSibling(stem + ".pdf"))) { + orphanDecks.add(GENERATED_ROOT.relativize(deck).toString().replace('\\', '/')); + } + } + } + + assertThat(orphanDecks) + .describedAs("a .pptx with no same-named .pdf beside it never reaches the " + + "published showcase — ShowcaseSync keys every card on a PDF and " + + "copies a deck only as that PDF's companion") + .isEmpty(); + } + + /** Guards against a runner that silently narrows to one corner of the catalogue. */ + @Test + void generatedCatalogueCoversEveryCategory() throws IOException { + try (Stream walk = Files.walk(GENERATED_ROOT)) { + Set categories = walk + .filter(path -> path.toString().endsWith(".pdf")) + .map(path -> GENERATED_ROOT.relativize(path).getName(0).toString()) + .collect(Collectors.toCollection(TreeSet::new)); - // Sanity: at least one PDF should have been written. Each - // example resolves its own output path inside generate(), so we - // do not assert a fixed file list — that would couple this - // smoke test to the example list. Reaching this assertion at - // all means main() completed. - assertThat(true).isTrue(); + assertThat(categories) + .describedAs("the runner should cover the whole catalogue, not one corner") + .contains("templates", "features", "flagships"); + } } } diff --git a/scripts/cut-release.ps1 b/scripts/cut-release.ps1 index 60ab7953e..b1d6da09b 100644 --- a/scripts/cut-release.ps1 +++ b/scripts/cut-release.ps1 @@ -560,6 +560,7 @@ function Run-ShowcaseSync { # lifecycle phase. Wrapping the whole token in quotes preserves it # as a single literal argument. $execProp = '"-Dexec.mainClass=com.demcha.examples.support.ShowcaseSync"' + $generateProp = '"-Dexec.mainClass=com.demcha.examples.GenerateAllExamples"' # The examples module depends on these bumped SNAPSHOT siblings (not on # Central); each must be installed before exec:java can resolve them. Keep # this list in lockstep with examples/pom.xml — a module the examples depend @@ -574,6 +575,7 @@ function Run-ShowcaseSync { foreach ($modulePom in $exampleSnapshotSiblings) { Write-Host " [DRY RUN] $mvnw -B -ntp -DskipTests install -f $modulePom" -ForegroundColor Yellow } + Write-Host " [DRY RUN] $mvnw -B -ntp -f examples/pom.xml -DskipTests clean compile exec:java $generateProp" -ForegroundColor Yellow Write-Host " [DRY RUN] $mvnw -B -ntp -f examples/pom.xml -DskipTests compile exec:java $execProp" -ForegroundColor Yellow return } @@ -608,6 +610,23 @@ function Run-ShowcaseSync { throw "Install $modulePom failed (exit $LASTEXITCODE)" } } + # Regenerate the catalogue before syncing it. ShowcaseSync only COPIES what is + # already in examples/target/generated-pdfs, so without this the published site + # is whatever happens to be sitting in the maintainer's target directory from + # earlier commands — and on a clean checkout there is nothing there at all. + # `clean` is the point: it drops artifacts left by tests or by examples that + # have since been renamed or deleted, so the published tree is a function of + # the tag rather than of shell history. This runs after Step 1 bumped the poms + # and Step 3 rewrote GH_BASE, so version-stamped renders carry the new version. + Write-Host " > $mvnw -B -ntp -f examples/pom.xml -DskipTests clean compile exec:java $generateProp" -ForegroundColor DarkGray + & $mvnw -B -ntp -f examples/pom.xml -DskipTests clean compile exec:java $generateProp 2>&1 | ForEach-Object { + if ($_ -match 'BUILD SUCCESS|BUILD FAILURE|ERROR') { + Write-Host " $_" -ForegroundColor DarkGray + } + } + if ($LASTEXITCODE -ne 0) { + throw "GenerateAllExamples failed (exit $LASTEXITCODE)" + } # `compile` before exec:java is REQUIRED: Step 3 rewrote ShowcaseMetadata.GH_BASE # to /blob/, and exec:java runs the COMPILED class. Without recompiling it here, # ShowcaseSync would emit examples.json with the previous release's "View Code" links diff --git a/web/examples.json b/web/examples.json index 7f59d60ce..817a5e353 100644 --- a/web/examples.json +++ b/web/examples.json @@ -178,12 +178,12 @@ }, { "id": "proposal-modern-v2", - "title": "Proposal Modern V2", - "description": "Generated showcase for templates / proposal.", - "tags": ["templates", "proposal"], + "title": "Modern Proposal", + "description": "The ModernProposal preset composed straight from its document spec — cover, scope sections and pricing table themed through BrandTheme.", + "tags": ["proposal"], "pdf": "showcase/pdf/templates/proposal/proposal-modern-v2.pdf", "screenshot": "showcase/screenshots/templates/proposal/proposal-modern-v2.png", - "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples" + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/templates/proposal/v2/ModernProposalV2Example.java" } ] }, @@ -202,12 +202,12 @@ }, { "id": "invoice-modern-v2", - "title": "Invoice Modern V2", - "description": "Generated showcase for templates / invoice.", - "tags": ["templates", "invoice"], + "title": "Modern Invoice", + "description": "The ModernInvoice preset composed straight from an InvoiceDocumentSpec — line items, totals and payment block driven by the BrandTheme rather than per-document styling.", + "tags": ["invoice"], "pdf": "showcase/pdf/templates/invoice/invoice-modern-v2.pdf", "screenshot": "showcase/screenshots/templates/invoice/invoice-modern-v2.png", - "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples" + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/templates/invoice/v2/ModernInvoiceV2Example.java" } ] }, @@ -403,12 +403,12 @@ }, { "id": "inline-code-column-wrap", - "title": "Inline Code Column Wrap", - "description": "Generated showcase for features / tables.", - "tags": ["features", "tables"], + "title": "Inline Code in Narrow Columns", + "description": "Inline-code chips inside a fixed-width column — how a token too wide for its cell wraps instead of overflowing the column edge.", + "tags": ["tables", "text"], "pdf": "showcase/pdf/features/tables/inline-code-column-wrap.pdf", "screenshot": "showcase/screenshots/features/tables/inline-code-column-wrap.png", - "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples" + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java" }, { "id": "table-advanced", @@ -859,12 +859,12 @@ }, { "id": "poetry-title", - "title": "Poetry Title", - "description": "Generated showcase for features / title.", - "tags": ["features", "title"], + "title": "Poetry Title Page", + "description": "A centred title page with generous vertical rhythm — margins, alignment and spacing carrying the composition instead of decoration.", + "tags": ["title"], "pdf": "showcase/pdf/features/title/poetry-title.pdf", "screenshot": "showcase/screenshots/features/title/poetry-title.png", - "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples" + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/features/title/PoetryTitlePageExample.java" } ] } @@ -900,12 +900,12 @@ }, { "id": "engine-deck-v2", - "title": "Engine Deck V2", - "description": "Generated showcase for flagships / default.", - "tags": ["flagships", "default"], + "title": "Engine Deck — Module First", + "description": "The landscape deck the README banner is cut from: the 2.0 module graph, native vector charts, and comparative benchmark figures read from the committed snapshot at render time.", + "tags": ["flagship", "showcase"], "pdf": "showcase/pdf/flagships/default/engine-deck-v2.pdf", "screenshot": "showcase/screenshots/flagships/default/engine-deck-v2.png", - "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples" + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/EngineDeckV2Example.java" }, { "id": "feature-catalog", @@ -926,6 +926,16 @@ "screenshot": "showcase/screenshots/flagships/default/financial-report.png", "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/FinancialReportExample.java" }, + { + "id": "linkedin-carousel", + "title": "LinkedIn Carousel", + "description": "A six-slide 4:5 carousel sized for a LinkedIn document post, typeset for a phone. Every figure is read at render time — the version from the filtered properties, the timings from the committed benchmark snapshot.", + "tags": ["flagship", "showcase"], + "pdf": "showcase/pdf/flagships/default/linkedin-carousel.pdf", + "pptx": "showcase/pptx/flagships/default/linkedin-carousel.pptx", + "screenshot": "showcase/screenshots/flagships/default/linkedin-carousel.png", + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/LinkedInCarouselExample.java" + }, { "id": "master-showcase", "title": "Master Showcase", @@ -936,6 +946,16 @@ "screenshot": "showcase/screenshots/flagships/default/master-showcase.png", "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/MasterShowcaseExample.java" }, + { + "id": "maven-banner", + "title": "Maven Central Banner", + "description": "A single-slide brand banner emitted through the PPTX backend — gradient, rounded panels, native paths and text frames arriving in PowerPoint as an editable copy of the rendered page.", + "tags": ["flagship", "showcase", "pptx"], + "pdf": "showcase/pdf/flagships/default/maven-banner.pdf", + "pptx": "showcase/pptx/flagships/default/maven-banner.pptx", + "screenshot": "showcase/screenshots/flagships/default/maven-banner.png", + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java" + }, { "id": "module-first-profile", "title": "Module-First Authoring", @@ -945,6 +965,16 @@ "screenshot": "showcase/screenshots/flagships/default/module-first-profile.png", "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/ModuleFirstFileExample.java" }, + { + "id": "social-card", + "title": "Social Preview Card", + "description": "The repository's 1280x640 social preview, itself a GraphCompose document — one sheet resolving into a portrait page and a 16:9 slide from the same content, so the card cannot drift from the palette and wordmark it is drawn with.", + "tags": ["flagship", "showcase"], + "pdf": "showcase/pdf/flagships/default/social-card.pdf", + "pptx": "showcase/pptx/flagships/default/social-card.pptx", + "screenshot": "showcase/screenshots/flagships/default/social-card.png", + "code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/SocialCardExample.java" + }, { "id": "twin-output", "title": "Twin Output", diff --git a/web/showcase/pdf/features/chrome/pdf-chrome.pdf b/web/showcase/pdf/features/chrome/pdf-chrome.pdf index b0df47089..aa10c0999 100644 Binary files a/web/showcase/pdf/features/chrome/pdf-chrome.pdf and b/web/showcase/pdf/features/chrome/pdf-chrome.pdf differ diff --git a/web/showcase/pdf/features/shapes/shape-container.pdf b/web/showcase/pdf/features/shapes/shape-container.pdf index a8b3a74f7..a9b31e9bc 100644 Binary files a/web/showcase/pdf/features/shapes/shape-container.pdf and b/web/showcase/pdf/features/shapes/shape-container.pdf differ diff --git a/web/showcase/pdf/features/tables/inline-code-column-wrap.pdf b/web/showcase/pdf/features/tables/inline-code-column-wrap.pdf index e74a7aaa5..e23ad70a6 100644 Binary files a/web/showcase/pdf/features/tables/inline-code-column-wrap.pdf and b/web/showcase/pdf/features/tables/inline-code-column-wrap.pdf differ diff --git a/web/showcase/pdf/features/text/section-presets.pdf b/web/showcase/pdf/features/text/section-presets.pdf index 2978d4717..57c8d8e3a 100644 Binary files a/web/showcase/pdf/features/text/section-presets.pdf and b/web/showcase/pdf/features/text/section-presets.pdf differ diff --git a/web/showcase/pdf/features/themes/invoice-custom-theme.pdf b/web/showcase/pdf/features/themes/invoice-custom-theme.pdf deleted file mode 100644 index b8526be12..000000000 Binary files a/web/showcase/pdf/features/themes/invoice-custom-theme.pdf and /dev/null differ diff --git a/web/showcase/pdf/features/title/poetry-title.pdf b/web/showcase/pdf/features/title/poetry-title.pdf index c72b3e84b..87151967e 100644 Binary files a/web/showcase/pdf/features/title/poetry-title.pdf and b/web/showcase/pdf/features/title/poetry-title.pdf differ diff --git a/web/showcase/pdf/flagships/default/engine-deck-v2.pdf b/web/showcase/pdf/flagships/default/engine-deck-v2.pdf index ede78caf8..b0ba395e4 100644 Binary files a/web/showcase/pdf/flagships/default/engine-deck-v2.pdf and b/web/showcase/pdf/flagships/default/engine-deck-v2.pdf differ diff --git a/web/showcase/pdf/flagships/default/linkedin-carousel.pdf b/web/showcase/pdf/flagships/default/linkedin-carousel.pdf new file mode 100644 index 000000000..5dbeb7396 Binary files /dev/null and b/web/showcase/pdf/flagships/default/linkedin-carousel.pdf differ diff --git a/web/showcase/pdf/flagships/default/maven-banner.pdf b/web/showcase/pdf/flagships/default/maven-banner.pdf new file mode 100644 index 000000000..0a9213c48 Binary files /dev/null and b/web/showcase/pdf/flagships/default/maven-banner.pdf differ diff --git a/web/showcase/pdf/flagships/default/social-card.pdf b/web/showcase/pdf/flagships/default/social-card.pdf new file mode 100644 index 000000000..d0592c7c7 Binary files /dev/null and b/web/showcase/pdf/flagships/default/social-card.pdf differ diff --git a/web/showcase/pdf/templates/invoice/invoice-modern-v2.pdf b/web/showcase/pdf/templates/invoice/invoice-modern-v2.pdf index 49edc4016..c9e348018 100644 Binary files a/web/showcase/pdf/templates/invoice/invoice-modern-v2.pdf and b/web/showcase/pdf/templates/invoice/invoice-modern-v2.pdf differ diff --git a/web/showcase/pdf/templates/invoice/invoice.pdf b/web/showcase/pdf/templates/invoice/invoice.pdf deleted file mode 100644 index 91cb652c6..000000000 Binary files a/web/showcase/pdf/templates/invoice/invoice.pdf and /dev/null differ diff --git a/web/showcase/pdf/templates/proposal/project-proposal-cinematic.pdf b/web/showcase/pdf/templates/proposal/project-proposal-cinematic.pdf index 5bdd45d03..ea4a0a0f5 100644 Binary files a/web/showcase/pdf/templates/proposal/project-proposal-cinematic.pdf and b/web/showcase/pdf/templates/proposal/project-proposal-cinematic.pdf differ diff --git a/web/showcase/pdf/templates/proposal/proposal-cinematic.pdf b/web/showcase/pdf/templates/proposal/proposal-cinematic.pdf index db9cb6789..30ccf8ac9 100644 Binary files a/web/showcase/pdf/templates/proposal/proposal-cinematic.pdf and b/web/showcase/pdf/templates/proposal/proposal-cinematic.pdf differ diff --git a/web/showcase/pdf/templates/proposal/proposal-modern-v2.pdf b/web/showcase/pdf/templates/proposal/proposal-modern-v2.pdf index 4eabcf925..ada0d7970 100644 Binary files a/web/showcase/pdf/templates/proposal/proposal-modern-v2.pdf and b/web/showcase/pdf/templates/proposal/proposal-modern-v2.pdf differ diff --git a/web/showcase/pdf/templates/proposal/proposal.pdf b/web/showcase/pdf/templates/proposal/proposal.pdf deleted file mode 100644 index 3d18c951b..000000000 Binary files a/web/showcase/pdf/templates/proposal/proposal.pdf and /dev/null differ diff --git a/web/showcase/pptx/flagships/default/linkedin-carousel.pptx b/web/showcase/pptx/flagships/default/linkedin-carousel.pptx new file mode 100644 index 000000000..7e0602228 Binary files /dev/null and b/web/showcase/pptx/flagships/default/linkedin-carousel.pptx differ diff --git a/web/showcase/pptx/flagships/default/maven-banner.pptx b/web/showcase/pptx/flagships/default/maven-banner.pptx new file mode 100644 index 000000000..fd0a5375b Binary files /dev/null and b/web/showcase/pptx/flagships/default/maven-banner.pptx differ diff --git a/web/showcase/pptx/flagships/default/social-card.pptx b/web/showcase/pptx/flagships/default/social-card.pptx new file mode 100644 index 000000000..b9f878cc5 Binary files /dev/null and b/web/showcase/pptx/flagships/default/social-card.pptx differ diff --git a/web/showcase/screenshots/features/chrome/pdf-chrome.png b/web/showcase/screenshots/features/chrome/pdf-chrome.png index 049c5b329..9fc770347 100644 Binary files a/web/showcase/screenshots/features/chrome/pdf-chrome.png and b/web/showcase/screenshots/features/chrome/pdf-chrome.png differ diff --git a/web/showcase/screenshots/features/themes/invoice-custom-theme.png b/web/showcase/screenshots/features/themes/invoice-custom-theme.png deleted file mode 100644 index 2d9ca7366..000000000 Binary files a/web/showcase/screenshots/features/themes/invoice-custom-theme.png and /dev/null differ diff --git a/web/showcase/screenshots/flagships/default/engine-deck-v2.png b/web/showcase/screenshots/flagships/default/engine-deck-v2.png index b7f2e8d07..d4fb6632b 100644 Binary files a/web/showcase/screenshots/flagships/default/engine-deck-v2.png and b/web/showcase/screenshots/flagships/default/engine-deck-v2.png differ diff --git a/web/showcase/screenshots/flagships/default/linkedin-carousel.png b/web/showcase/screenshots/flagships/default/linkedin-carousel.png new file mode 100644 index 000000000..2bab09202 Binary files /dev/null and b/web/showcase/screenshots/flagships/default/linkedin-carousel.png differ diff --git a/web/showcase/screenshots/flagships/default/maven-banner.png b/web/showcase/screenshots/flagships/default/maven-banner.png new file mode 100644 index 000000000..901995ab8 Binary files /dev/null and b/web/showcase/screenshots/flagships/default/maven-banner.png differ diff --git a/web/showcase/screenshots/flagships/default/social-card.png b/web/showcase/screenshots/flagships/default/social-card.png new file mode 100644 index 000000000..999ddd86f Binary files /dev/null and b/web/showcase/screenshots/flagships/default/social-card.png differ diff --git a/web/showcase/screenshots/templates/invoice/invoice.png b/web/showcase/screenshots/templates/invoice/invoice.png deleted file mode 100644 index b4f3168c1..000000000 Binary files a/web/showcase/screenshots/templates/invoice/invoice.png and /dev/null differ diff --git a/web/showcase/screenshots/templates/proposal/proposal.png b/web/showcase/screenshots/templates/proposal/proposal.png deleted file mode 100644 index 2dca5f466..000000000 Binary files a/web/showcase/screenshots/templates/proposal/proposal.png and /dev/null differ