diff --git a/.gitignore b/.gitignore index c9f7fc1e..57ac5508 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,10 @@ benchmarks/logs/ # 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. +!web/showcase/**/*.pdf /patch-root-position.diff /rectangle.pdf /Test.pdf diff --git a/docs/first-document.md b/docs/first-document.md index 1e7cb59f..cf94f03c 100644 --- a/docs/first-document.md +++ b/docs/first-document.md @@ -12,6 +12,7 @@ with a page flow, and render. No coordinates, no manual page breaks. Open a session for a file path, add one page flow, render. The engine handles placement and pagination. + ```java import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentPageSize; @@ -41,6 +42,7 @@ modules, sections, paragraphs, lists, tables, and rows are added top to bottom. The same Flow model scales to a multi-section document. There are still no coordinates and no manual page breaks — just structure in reading order. + ```java import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentPageSize; diff --git a/docs/templates/business-templates.md b/docs/templates/business-templates.md index d61d902d..27db24ac 100644 --- a/docs/templates/business-templates.md +++ b/docs/templates/business-templates.md @@ -30,6 +30,7 @@ decides file vs stream vs bytes. The caller does. ## Invoice + ```java import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentPageSize; @@ -91,6 +92,7 @@ Same shape, different spec. Use a proposal when the artifact is sales or project scope rather than billing. The timeline takes a three-argument `timelineItem(phase, duration, details)`. + ```java import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentPageSize; @@ -147,6 +149,7 @@ document is streamed to the caller's stream. The template composes the same way before any output method; create one session per request. `create()` without arguments uses the family's default theme. + ```java import com.demcha.compose.GraphCompose; import com.demcha.compose.document.api.DocumentSession; 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 ffb05cb5..fb4353ac 100644 --- a/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java +++ b/examples/src/main/java/com/demcha/examples/support/ShowcaseMetadata.java @@ -78,39 +78,67 @@ 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", "Polished V2 invoice template with theme-driven layout, advanced tables, and totals.", "invoice", "cinematic"); + invoice("invoice-cinematic", "Cinematic Invoice", "Layered ModernInvoice preset with theme-driven layout, advanced tables, and totals.", "invoice", "cinematic"); // ===== Templates / Proposal ===== - proposal("proposal-cinematic", "Cinematic Proposal", "Cinematic V2 proposal layout with cover panel, hero spread, and rich typography.", "proposal", "cinematic"); + 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"); // ===== Templates / Schedule ===== schedule("weekly-schedule", "Weekly Schedule", "Multi-day weekly schedule with shift assignments, category fills, and repeated header.", "schedule", "table"); // ===== Features ===== - feature("lists", "nested-list-showcase", "Nested Lists", "ListBuilder.addItem(label, Consumer) — depth cascade, per-depth markers, mixed flat / nested authoring.", "lists", "v1.6"); - feature("tables", "table-advanced", "Advanced Tables", "Row span, column span, zebra rows, total rows, and repeating headers across page breaks.", "tables", "pagination"); - feature("tables", "composed-table-cell-showcase", "Composed Table Cells", "DocumentTableCell.node(DocumentNode) — paragraphs, lists, sub-tables inside cells with two-pass measurement.", "tables", "v1.6"); - feature("canvas", "canvas-layer-showcase", "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", "Shape-as-Container", "Rounded rect, ellipse, circle containers with ClipPolicy and layered children.", "shapes", "clip"); - feature("svg", "svg-icon-gallery", "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"); - feature("shapes", "vector-path", "Vector Paths (Bézier)", "addPath(...) — free-form design shapes with native cubic Bézier curves: stroked waves, filled blobs, mixed line/curve ribbons. No tessellation.", "shapes", "bezier", "v1.8"); - feature("shapes", "photo-clip", "Photo Clip (silhouette)", "A raster photo clipped to a free-form silhouette — circle, SVG heart, star — via ShapeContainer.path(...) + ClipPolicy.CLIP_PATH; the image COVER-fills each box so the native-curve outline crops it crisply at any zoom.", "shapes", "clip", "v1.8"); - feature("layout", "block-align", "Block Alignment", "addAligned(align, node) / addSvgIcon(icon, w, align) — seat any fixed-size node left / centre / right across the content width.", "layout", "align", "v1.8"); - feature("transforms", "transforms", "Layers + Transforms", "rotate / scale on every leaf builder + LayerStack with explicit z-index.", "transforms", "layers"); - feature("text", "rich-text-showcase", "Rich Text", "Inline runs with bold / italic / colour / link options, markdown parsing.", "text", "rich"); - feature("text", "section-presets", "Section Presets", "Pre-baked section bands, accent strips, soft panels for templates.", "text", "sections"); - feature("barcodes", "barcode-showcase", "Barcodes & QR", "QR code, Code128, EAN-13, PDF417 — every supported barcode + per-barcode styling.", "barcodes", "qr"); - feature("chrome", "pdf-chrome", "PDF Chrome", "Headers, footers, watermarks, metadata, document protection / encryption.", "chrome", "metadata", "watermark"); - feature("streaming", "invoice-http-stream", "HTTP Streaming", "Stream PDF directly to a Servlet response with no buffering.", "streaming", "http"); - feature("snapshots", "invoice-snapshot-regression", "Layout Snapshots", "How LayoutSnapshotAssertions captures the resolved layout graph for regression testing.", "snapshots", "testing"); - feature("docx", "word-export-companion", "Word Export (DOCX)", "DocxSemanticBackend — the same document as a fixed-layout PDF and an editable Word file; charts fall back to their data table.", "docx", "word", "export"); - feature("debug", "debug-overlay", "Debug Overlay", "DocumentDebugOptions — guide lines plus semantic node-path labels on the rendered sheet; trace any misplaced block back to the builder call that authored it.", "debug", "labels", "v1.8"); + 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("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"); + feature("shapes", "vector-path", "VectorPathExample", "Vector Paths (Bézier)", "addPath(...) — free-form design shapes with native cubic Bézier curves: stroked waves, filled blobs, mixed line/curve ribbons. No tessellation.", "shapes", "bezier", "v1.8"); + feature("shapes", "photo-clip", "PhotoClipExample", "Photo Clip (silhouette)", "A raster photo clipped to a free-form silhouette — circle, SVG heart, star — via ShapeContainer.path(...) + ClipPolicy.CLIP_PATH; the image COVER-fills each box so the native-curve outline crops it crisply at any zoom.", "shapes", "clip", "v1.8"); + feature("layout", "block-align", "BlockAlignExample", "Block Alignment", "addAligned(align, node) / addSvgIcon(icon, w, align) — seat any fixed-size node left / centre / right across the content width.", "layout", "align", "v1.8"); + feature("transforms", "transforms", "TransformsExample", "Layers + Transforms", "rotate / scale on every leaf builder + LayerStack with explicit z-index.", "transforms", "layers"); + feature("text", "rich-text-showcase", "RichTextShowcaseExample", "Rich Text", "Inline runs with bold / italic / colour / link options, markdown parsing.", "text", "rich"); + feature("text", "section-presets", "SectionPresetsExample", "Section Presets", "Pre-baked section bands, accent strips, soft panels for templates.", "text", "sections"); + feature("barcodes", "barcode-showcase", "BarcodeShowcaseExample", "Barcodes & QR", "QR code, Code128, EAN-13, PDF417 — every supported barcode + per-barcode styling.", "barcodes", "qr"); + feature("chrome", "pdf-chrome", "PdfChromeExample", "PDF Chrome", "Headers, footers, watermarks, metadata, document protection / encryption.", "chrome", "metadata", "watermark"); + feature("streaming", "invoice-http-stream", "HttpStreamingExample", "HTTP Streaming", "Stream PDF directly to a Servlet response with no buffering.", "streaming", "http"); + feature("snapshots", "invoice-snapshot-regression", "LayoutSnapshotRegressionExample", "Layout Snapshots", "How LayoutSnapshotAssertions captures the resolved layout graph for regression testing.", "snapshots", "testing"); + feature("docx", "word-export-companion", "WordExportExample", "Word Export (DOCX)", "DocxSemanticBackend — the same document as a fixed-layout PDF and an editable Word file; charts fall back to their data table.", "docx", "word", "export"); + feature("debug", "debug-overlay", "DebugOverlayExample", "Debug Overlay", "DocumentDebugOptions — guide lines plus semantic node-path labels on the rendered sheet; trace any misplaced block back to the builder call that authored it.", "debug", "labels", "v1.8"); + + // --- feature examples added since v1.7 --- + feature("shapes", "line-cap", "LineCapExample", "Line Caps & Dotted Lines", "addLine(...).lineCap(DocumentLineCap.ROUND) — round/square end-caps on plain lines; a ROUND cap on a near-zero dashed(0.1, 4) rule draws the classic dotted TOC leader.", "shapes", "lines", "v1.9"); + feature("shapes", "line-fill", "LineFillExample", "Fill Lines & Dot Leaders", "line().fill() — a rule or dotted leader stretches to the content width or its weighted row slot, drawing table-of-contents leaders without measuring the gap by hand.", "shapes", "layout", "v1.9"); + feature("text", "emoji-clip-path", "EmojiClipPathReportExample", "Emoji Clip-Path Report", "ParagraphBuilder.rich(r -> r.svgIcon(SvgIcon.parse(xml), 22)) — drops parsed SVG emoji glyphs inline inside table cells to audit which clip-path glyphs render.", "emoji", "inline", "v1.9"); + feature("text", "emoji-gallery", "EmojiGalleryExample", "Emoji Gallery", "inlineSvgIcon(SvgIcon, pt) — flows the full graph-compose-emoji set as inline vector glyphs, paginated across pages with SvgIcon.parse from the classpath index.", "emoji", "inline", "v1.9"); + feature("text", "emoji-shortcodes", "EmojiShortcodeExample", "Emoji Shortcodes", "RichText.emoji(\":rocket:\", size) — resolves a GitHub-style shortcode to an inline vector colour glyph on the text baseline; unknown codes fall back to literal text.", "emoji", "inline", "v1.9"); + feature("text", "emoji-svg-vs-png", "EmojiSvgVsPngExample", "Emoji SVG vs PNG", "RichText.svgIcon(SvgIcon, pt) draws the same starter emoji as crisp inline vector beside RichText.image(...) raster bytes from toImage(...), one glyph down two inline paths.", "emoji", "inline", "v1.9"); + feature("text", "inline-highlight-chips", "InlineHighlightExample", "Inline Highlight Chips", "RichText.highlight(text, style, bg, radius, padding) — baseline-flowing rounded chips, with code(text) and chip(text, fg, bg) shorthands; multi-word fills wrap per line fragment.", "inline", "richtext", "v1.9"); + feature("text", "inline-shapes", "InlineShapesExample", "Inline Shape Runs", "RichText.dot/arrow/chevron/checkbox/shape(ShapeOutline) — geometric figures drawn on the text baseline from geometry, no font glyphs, between text and as list bullets.", "inline", "richtext", "v1.7"); + feature("text", "inline-svg-icons", "InlineSvgIconExample", "Inline SVG Icons", "RichText.svgIcon(SvgIcon, size) — multi-colour vector glyphs (gradients included) drawn on the text baseline from SVG, not font glyphs; crisp at any zoom, the engine path for vector colour emoji.", "v1.9", "inline", "svg"); + feature("chrome", "page-numbering", "PageNumberingExample", "Page Numbering", "DocumentHeaderFooter.numbering(DocumentPageNumbering) — {page}/{pages} footer tokens with countFrom(2) leaving the cover uncounted and LOWER_ROMAN restyling the body.", "chrome", "footer", "v1.9"); + feature("chrome", "viewer-preferences", "ViewerPreferencesExample", "Viewer Preferences", "chrome().viewerPreferences(DocumentViewerPreferences) — opens the PDF with pageMode(USE_OUTLINES) showing the bookmark panel and displayDocTitle(true) in the title bar.", "chrome", "v1.9"); + feature("charts", "chart-showcase", "ChartShowcaseExample", "Vector Charts", "section.chart(ChartSpec, ChartStyle) — bar/line/area/pie/donut specs (ChartSpec.bar(), LineInterpolation.MONOTONE, SliceLabelMode) compiled into engine primitives, no chart-specific render code.", "charts", "data-viz", "v1.9"); + feature("layout", "content-bleed", "BleedExample", "Content Bleed", "section.bleedToEdge(TOP, LEFT, RIGHT) and bleed(DocumentBleed.of(...)) push a fill to the trimmed page edge while children stay in the content margin.", "layout", "v1.9", "bleed"); + feature("layout", "per-page-margin", "PerPageMarginExample", "Per-Page Margins", "document.pageMargins(List.of(PageMarginRule.page(1, DocumentInsets.zero()), PageMarginRule.from(2, ...))) — a full-bleed cover then wide book margins in one session.", "layout", "v1.9"); + feature("layout", "row-columns", "RowColumnsExample", "Row Columns", "columns(auto(), weight(1), auto()) sizes row cells as intrinsic or weighted shares; paired with line().fill() a dotted leader fills the gap for a measureless table-of-contents row.", "layout", "v1.9", "rows"); + feature("layout", "row-flex", "RowFlexExample", "Row Flex & Arrangement", "RowBuilder.pushRight()/flexSpacer() springs absorb leftover row width while arrangement(RowArrangement.SPACE_BETWEEN/CENTER) justifies content-sized children — no manual coordinates.", "layout", "v1.9", "row"); + feature("layout", "row-vertical-align", "RowVerticalAlignExample", "Row Vertical Align", "RowBuilder.verticalAlign(RowVerticalAlign) — seats a row's children TOP, CENTER, or BOTTOM within the band set by the tallest child, no manual coordinates.", "layout", "v1.9"); + feature("navigation", "container-bookmark", "ContainerBookmarkExample", "PDF Outline Bookmarks", "bookmark(new DocumentBookmarkOptions(title)) on a container flow — each section becomes a PDF outline entry targeting its start page, building a navigable viewer panel with no manual coordinates.", "navigation", "structure", "v1.9"); + feature("navigation", "in-pdf-navigation", "InPdfNavigationExample", "In-PDF Navigation", "anchor(...) plus linkTo(...) — named destinations and internal go-to links for a clickable table of contents and bidirectional footnotes, resolved in a deferred pass for forward references.", "navigation", "v1.9", "links"); + 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", "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 ===== - flagship("master-showcase", "Master Showcase", "Kitchen-sink demo combining every primitive into a single document — the full GraphCompose surface.", "showcase"); - flagship("business-report", "Business Report Cover", "Flagship cover page with hero panel, KPI table, and accent strip — ready-to-ship template.", "showcase", "cover"); - flagship("module-first-profile", "Module-First Authoring", "Authoring style focused on declaring data modules first, layout second.", "authoring"); + flagship("master-showcase", "MasterShowcaseExample", "Master Showcase", "Kitchen-sink demo combining every primitive into a single document — the full GraphCompose surface.", "showcase"); + 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("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("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"); } static Entry lookup(String basename, String category, String group) { @@ -139,12 +167,18 @@ static String groupLabel(String category, String group) { case "features/transforms" -> "Transforms & Layers"; case "features/text" -> "Rich Text"; case "features/barcodes" -> "Barcodes & QR"; + case "features/themes" -> "Themes"; case "features/chrome" -> "PDF Chrome (header / footer / watermark)"; case "features/streaming" -> "Streaming & I/O"; case "features/snapshots" -> "Snapshot Testing"; case "features/svg" -> "SVG Import"; case "features/layout" -> "Layout & Alignment"; case "features/debug" -> "Debug & Diagnostics"; + case "features/charts" -> "Charts"; + case "features/navigation" -> "In-Document Navigation"; + case "features/structure" -> "Document Structure"; + case "features/title" -> "Title & Book Pages"; + case "features/docx" -> "Word Export (DOCX)"; case "flagships/default" -> "Flagship Demos"; default -> capitalize(group); }; @@ -195,33 +229,14 @@ private static void schedule(String id, String title, String desc, String... tag EX_BASE + "/templates/schedule/WeeklyScheduleFileExample.java")); } - private static void feature(String group, String id, String title, String desc, String... tags) { - String file = switch (group) { - case "lists" -> "lists/NestedListExample.java"; - case "tables" -> id.contains("composed") ? "tables/ComposedTableCellExample.java" : "tables/TableAdvancedExample.java"; - case "canvas" -> "canvas/CanvasLayerExample.java"; - case "shapes" -> id.equals("photo-clip") ? "shapes/PhotoClipExample.java" : "shapes/ShapeContainerExample.java"; - case "transforms" -> "transforms/TransformsExample.java"; - case "text" -> id.equals("section-presets") ? "text/SectionPresetsExample.java" : "text/RichTextShowcaseExample.java"; - case "barcodes" -> "barcodes/BarcodeShowcaseExample.java"; - case "chrome" -> "chrome/PdfChromeExample.java"; - case "streaming" -> "streaming/HttpStreamingExample.java"; - case "snapshots" -> "snapshots/LayoutSnapshotRegressionExample.java"; - case "docx" -> "docx/WordExportExample.java"; - default -> group + "/" + capitalize(id); - }; - ENTRIES.put(id, entry(title, desc, withCategory(group, tags), EX_BASE + "/features/" + file)); + private static void feature(String group, String id, String exampleClass, String title, String desc, String... tags) { + ENTRIES.put(id, entry(title, desc, withCategory(group, tags), + EX_BASE + "/features/" + group + "/" + exampleClass + ".java")); } - private static void flagship(String id, String title, String desc, String... tags) { - String file = switch (id) { - case "master-showcase" -> "MasterShowcaseExample.java"; - case "business-report" -> "BusinessReportExample.java"; - case "module-first-profile" -> "ModuleFirstFileExample.java"; - default -> id + ".java"; - }; + private static void flagship(String id, String exampleClass, String title, String desc, String... tags) { ENTRIES.put(id, entry(title, desc, withCategory("flagship", tags), - EX_BASE + "/flagships/" + file)); + EX_BASE + "/flagships/" + exampleClass + ".java")); } private static Entry entry(String title, String desc, List tags, String code) { diff --git a/src/test/java/com/demcha/documentation/DocumentationSnippetCompileTest.java b/src/test/java/com/demcha/documentation/DocumentationSnippetCompileTest.java new file mode 100644 index 00000000..e9d60f2a --- /dev/null +++ b/src/test/java/com/demcha/documentation/DocumentationSnippetCompileTest.java @@ -0,0 +1,378 @@ +package com.demcha.documentation; + +import org.junit.jupiter.api.Test; + +import javax.tools.Diagnostic; +import javax.tools.DiagnosticCollector; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileObject; +import javax.tools.SimpleJavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; +import javax.tools.ToolProvider; +import java.io.IOException; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Compiles the Java snippets published in {@code docs/} so that an API change + * which breaks a documented snippet fails the build instead of silently rotting + * the public docs. + * + *

Complements {@code DocumentationExamplesTest} (which renders hand-kept Java + * copies of representative examples): this guard reads the literal markdown + * fences, so the published page itself cannot drift from the API. + * + *

The guard is opt-in: only a fenced {@code java} block + * immediately preceded by an invisible marker comment is compiled — + * + *

{@code
+ * 
+ * ```java
+ * ...
+ * ```
+ * }
+ * + * The marker is an HTML comment, so it renders to nothing on GitHub and keeps + * the published page clean. Teaching fragments that intentionally reference + * symbols defined only in prose (a bare {@code invoice} variable, pseudo-code) + * carry no marker and are left untouched, which keeps the guard free of false + * positives. + * + *

Each marked block is wrapped into a compilation unit according to its + * {@code mode} and compiled in-memory against the test runtime classpath (the + * same canonical classes and dependencies the rest of the suite sees). Compiler + * errors fail the test; warnings are ignored. Leading {@code import} + * lines are lifted above the wrapper. + * + *

+ *
{@code mode=method}
statements; wrapped in a {@code void} method + * that {@code throws Exception}.
+ *
{@code mode=members}
field/method declarations; inserted as class + * members.
+ *
+ * + *

The guard self-tests both directions: {@link #compilerReportsErrorForBrokenSnippet()} + * proves a broken snippet is actually surfaced as a failure, and + * {@link #knownCanonicalTypeResolvesOnTestClasspath()} proves the compile classpath + * resolves canonical types — so a classpath regression is distinguishable from a + * real doc break. + */ +class DocumentationSnippetCompileTest { + + private static final Path PROJECT_ROOT = Path.of("").toAbsolutePath().normalize(); + private static final Path DOCS_ROOT = PROJECT_ROOT.resolve("docs"); + + private static final Pattern MARKER = + Pattern.compile("^\\s*$"); + private static final Pattern IMPORT_LINE = + Pattern.compile("^\\s*import\\s+(?:static\\s+)?[\\w.]+(?:\\.\\*)?\\s*;\\s*$"); + private static final Pattern JAVA_FENCE = + Pattern.compile("^```java\\s*$"); + private static final Set SUPPORTED_MODES = Set.of("method", "members"); + + @Test + void publishedJavaSnippetsShouldCompile() throws IOException { + List examples = collectExamples(); + + // The scan must find work — a silent zero would let the guard pass while + // covering nothing (e.g. a moved docs folder or a broken marker regex). + assertThat(examples) + .describedAs("No doc-example markers found under %s — the guard would cover nothing", DOCS_ROOT) + .isNotEmpty(); + + assertThat(compile(examples)) + .describedAs("Every marked Java snippet under docs/ must compile against the current API") + .isEmpty(); + } + + @Test + void compilerReportsErrorForBrokenSnippet() throws IOException { + // Drives the full mechanism (wrap -> compile -> collect -> attribute) on a + // snippet that references a symbol that does not exist. Proves the guard + // actually fails — and names the offending snippet — instead of passing + // vacuously if any stage regressed. + Example broken = new Example( + "synthetic-broken-snippet", "method", + "thisMethodDoesNotExistOnAnyType();\n", + PROJECT_ROOT.resolve("docs/(synthetic).md")); + + List errors = compile(List.of(broken)); + + assertThat(errors) + .describedAs("A snippet referencing a missing symbol must be reported as an error") + .isNotEmpty() + .allSatisfy(error -> assertThat(error).contains("synthetic-broken-snippet")); + } + + @Test + void knownCanonicalTypeResolvesOnTestClasspath() throws IOException { + // A trivial snippet that imports and calls a known canonical type. If this + // fails, the compile classpath is not resolving the library — a classpath / + // Surefire booter problem, NOT a documentation defect. Keeping it separate + // makes that distinction unambiguous when the suite goes red. + Example probe = new Example( + "synthetic-classpath-probe", "method", + "import com.demcha.compose.GraphCompose;\nGraphCompose.document();\n", + PROJECT_ROOT.resolve("docs/(synthetic).md")); + + assertThat(compile(List.of(probe))) + .describedAs("A known canonical type must resolve on the test classpath; " + + "a failure here is a classpath problem, not a docs problem") + .isEmpty(); + } + + @Test + void docExampleMarkersShouldBeWellFormed() throws IOException { + List problems = new ArrayList<>(); + Map idToFile = new LinkedHashMap<>(); + Map unitNameToId = new LinkedHashMap<>(); + + for (Path doc : markdownFiles()) { + List lines = Files.readAllLines(doc, StandardCharsets.UTF_8); + String rel = relative(doc); + for (int i = 0; i < lines.size(); i++) { + Matcher marker = MARKER.matcher(lines.get(i).trim()); + if (!marker.matches()) { + continue; + } + Map attributes = parseAttributes(marker.group(1)); + String id = attributes.get("id"); + String mode = attributes.get("mode"); + + if (id == null || id.isBlank()) { + problems.add("%s:%d — doc-example marker is missing an id".formatted(rel, i + 1)); + } else if (idToFile.containsKey(id)) { + problems.add("%s:%d — duplicate doc-example id '%s' (also in %s)" + .formatted(rel, i + 1, id, idToFile.get(id))); + } else { + idToFile.put(id, rel + ":" + (i + 1)); + String unitName = Example.unitNameFor(id); + String clash = unitNameToId.put(unitName, id); + if (clash != null) { + problems.add("%s:%d — doc-example id '%s' sanitizes to the same unit name as '%s'" + .formatted(rel, i + 1, id, clash)); + } + } + + if (mode == null || !SUPPORTED_MODES.contains(mode)) { + problems.add("%s:%d — doc-example '%s' has unsupported mode '%s' (use %s)" + .formatted(rel, i + 1, id, mode, SUPPORTED_MODES)); + } + + if (fenceAfter(lines, i) == null) { + problems.add("%s:%d — doc-example '%s' is not followed by a java fence" + .formatted(rel, i + 1, id)); + } + } + } + + assertThat(problems) + .describedAs("doc-example markers must be well-formed (unique id + unit name, supported mode, followed by a java fence)") + .isEmpty(); + } + + /** Compiles the given examples in-memory and returns one string per compiler error, attributed to its example. */ + private static List compile(List examples) throws IOException { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + assertThat(compiler) + .describedAs("A JDK compiler is required to compile doc snippets; run the build on a JDK, not a JRE") + .isNotNull(); + + DiagnosticCollector diagnostics = new DiagnosticCollector<>(); + StandardJavaFileManager fileManager = + compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8); + + Map bySource = new LinkedHashMap<>(); + List units = new ArrayList<>(); + for (Example example : examples) { + JavaFileObject source = new StringSource(example.unitName(), example.toCompilationUnit()); + bySource.put(source, example); + units.add(source); + } + + Path classOutput = null; + try { + classOutput = Files.createTempDirectory("doc-snippets-classes"); + fileManager.setLocation(StandardLocation.CLASS_OUTPUT, List.of(classOutput.toFile())); + List options = List.of( + "-proc:none", + "-classpath", System.getProperty("java.class.path")); + compiler.getTask(null, fileManager, diagnostics, options, null, units).call(); + } finally { + fileManager.close(); + if (classOutput != null) { + deleteRecursively(classOutput); + } + } + + List errors = new ArrayList<>(); + for (Diagnostic diagnostic : diagnostics.getDiagnostics()) { + if (diagnostic.getKind() != Diagnostic.Kind.ERROR) { + continue; + } + Example example = bySource.get(diagnostic.getSource()); + String origin = example == null + ? "(unknown unit)" + : example.id + " — " + relative(example.file); + errors.add("[%s] %s".formatted(origin, diagnostic.getMessage(null).replaceAll("\\s+", " ").trim())); + } + return errors; + } + + private List collectExamples() throws IOException { + List examples = new ArrayList<>(); + for (Path doc : markdownFiles()) { + List lines = Files.readAllLines(doc, StandardCharsets.UTF_8); + for (int i = 0; i < lines.size(); i++) { + Matcher marker = MARKER.matcher(lines.get(i).trim()); + if (!marker.matches()) { + continue; + } + Map attributes = parseAttributes(marker.group(1)); + String id = attributes.get("id"); + String mode = attributes.get("mode"); + if (id == null || id.isBlank() || mode == null || !SUPPORTED_MODES.contains(mode)) { + continue; // structural problems are reported by docExampleMarkersShouldBeWellFormed + } + String fence = fenceAfter(lines, i); + if (fence != null) { + examples.add(new Example(id, mode, fence, doc)); + } + } + } + return examples; + } + + private List markdownFiles() throws IOException { + if (!Files.isDirectory(DOCS_ROOT)) { + return List.of(); + } + try (Stream paths = Files.walk(DOCS_ROOT)) { + return paths.filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".md")) + .sorted() + .toList(); + } + } + + /** Returns the body of the next {@code java} fence after {@code markerIndex}, or null. */ + private static String fenceAfter(List lines, int markerIndex) { + int i = markerIndex + 1; + while (i < lines.size() && lines.get(i).isBlank()) { + i++; + } + if (i >= lines.size() || !JAVA_FENCE.matcher(lines.get(i).trim()).matches()) { + return null; + } + StringBuilder body = new StringBuilder(); + for (int j = i + 1; j < lines.size(); j++) { + if (lines.get(j).trim().equals("```")) { + return body.toString(); + } + body.append(lines.get(j)).append('\n'); + } + return null; // unterminated fence + } + + private static Map parseAttributes(String raw) { + Map attributes = new LinkedHashMap<>(); + for (String token : raw.trim().split("\\s+")) { + int eq = token.indexOf('='); + if (eq > 0) { + attributes.put(token.substring(0, eq), token.substring(eq + 1)); + } + } + return attributes; + } + + private static String relative(Path path) { + return PROJECT_ROOT.relativize(path).toString().replace('\\', '/'); + } + + private static void deleteRecursively(Path root) { + try (Stream paths = Files.walk(root)) { + paths.sorted((a, b) -> b.getNameCount() - a.getNameCount()).forEach(path -> { + try { + Files.deleteIfExists(path); + } catch (IOException ignored) { + // best-effort temp cleanup + } + }); + } catch (IOException ignored) { + // best-effort temp cleanup + } + } + + private record Example(String id, String mode, String fence, Path file) { + + static String unitNameFor(String id) { + return "DocExample_" + id.replaceAll("[^A-Za-z0-9]", "_"); + } + + String unitName() { + return unitNameFor(id); + } + + String toCompilationUnit() { + // Lift only the leading run of import lines; an import-shaped line that + // appears after real code (e.g. inside a text block) stays in the body. + List imports = new ArrayList<>(); + StringBuilder body = new StringBuilder(); + boolean inBody = false; + for (String line : fence.split("\\n", -1)) { + if (!inBody && IMPORT_LINE.matcher(line).matches()) { + imports.add(line.trim()); + continue; + } + if (!inBody && !line.isBlank()) { + inBody = true; + } + body.append(line).append('\n'); + } + + StringBuilder unit = new StringBuilder(); + for (String anImport : imports) { + unit.append(anImport).append('\n'); + } + unit.append('\n'); + unit.append("public final class ").append(unitName()).append(" {\n"); + if (mode.equals("method")) { + unit.append(" @SuppressWarnings({\"unused\", \"try\"})\n"); + unit.append(" void __example() throws Exception {\n"); + unit.append(body); + unit.append(" }\n"); + } else { // members + unit.append(body); + } + unit.append("}\n"); + return unit.toString(); + } + } + + private static final class StringSource extends SimpleJavaFileObject { + private final String code; + + StringSource(String unitName, String code) { + super(URI.create("string:///" + unitName.replace('.', '/') + Kind.SOURCE.extension), Kind.SOURCE); + this.code = code; + } + + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) { + return code; + } + } +} diff --git a/web/showcase/pdf/features/charts/chart-showcase.pdf b/web/showcase/pdf/features/charts/chart-showcase.pdf new file mode 100644 index 00000000..60b912ce Binary files /dev/null and b/web/showcase/pdf/features/charts/chart-showcase.pdf differ diff --git a/web/showcase/pdf/features/chrome/page-numbering.pdf b/web/showcase/pdf/features/chrome/page-numbering.pdf new file mode 100644 index 00000000..6f43f029 Binary files /dev/null and b/web/showcase/pdf/features/chrome/page-numbering.pdf differ diff --git a/web/showcase/pdf/features/chrome/viewer-preferences.pdf b/web/showcase/pdf/features/chrome/viewer-preferences.pdf new file mode 100644 index 00000000..917e2d94 Binary files /dev/null and b/web/showcase/pdf/features/chrome/viewer-preferences.pdf differ diff --git a/web/showcase/pdf/features/debug/debug-overlay.pdf b/web/showcase/pdf/features/debug/debug-overlay.pdf new file mode 100644 index 00000000..d4451cdf Binary files /dev/null and b/web/showcase/pdf/features/debug/debug-overlay.pdf differ diff --git a/web/showcase/pdf/features/docx/word-export-companion.pdf b/web/showcase/pdf/features/docx/word-export-companion.pdf new file mode 100644 index 00000000..1ceafb95 Binary files /dev/null and b/web/showcase/pdf/features/docx/word-export-companion.pdf differ diff --git a/web/showcase/pdf/features/layout/block-align.pdf b/web/showcase/pdf/features/layout/block-align.pdf new file mode 100644 index 00000000..f359a66f Binary files /dev/null and b/web/showcase/pdf/features/layout/block-align.pdf differ diff --git a/web/showcase/pdf/features/layout/content-bleed.pdf b/web/showcase/pdf/features/layout/content-bleed.pdf new file mode 100644 index 00000000..0e549774 Binary files /dev/null and b/web/showcase/pdf/features/layout/content-bleed.pdf differ diff --git a/web/showcase/pdf/features/layout/per-page-margin.pdf b/web/showcase/pdf/features/layout/per-page-margin.pdf new file mode 100644 index 00000000..e7f547dd Binary files /dev/null and b/web/showcase/pdf/features/layout/per-page-margin.pdf differ diff --git a/web/showcase/pdf/features/layout/row-columns.pdf b/web/showcase/pdf/features/layout/row-columns.pdf new file mode 100644 index 00000000..5f555636 Binary files /dev/null and b/web/showcase/pdf/features/layout/row-columns.pdf differ diff --git a/web/showcase/pdf/features/layout/row-flex.pdf b/web/showcase/pdf/features/layout/row-flex.pdf new file mode 100644 index 00000000..9a2198f7 Binary files /dev/null and b/web/showcase/pdf/features/layout/row-flex.pdf differ diff --git a/web/showcase/pdf/features/layout/row-vertical-align.pdf b/web/showcase/pdf/features/layout/row-vertical-align.pdf new file mode 100644 index 00000000..72e3174d Binary files /dev/null and b/web/showcase/pdf/features/layout/row-vertical-align.pdf differ diff --git a/web/showcase/pdf/features/navigation/container-bookmark.pdf b/web/showcase/pdf/features/navigation/container-bookmark.pdf new file mode 100644 index 00000000..6b7bb335 Binary files /dev/null and b/web/showcase/pdf/features/navigation/container-bookmark.pdf differ diff --git a/web/showcase/pdf/features/navigation/in-pdf-navigation.pdf b/web/showcase/pdf/features/navigation/in-pdf-navigation.pdf new file mode 100644 index 00000000..a98cd6be Binary files /dev/null and b/web/showcase/pdf/features/navigation/in-pdf-navigation.pdf differ diff --git a/web/showcase/pdf/features/navigation/page-reference.pdf b/web/showcase/pdf/features/navigation/page-reference.pdf new file mode 100644 index 00000000..d3b74320 Binary files /dev/null and b/web/showcase/pdf/features/navigation/page-reference.pdf differ diff --git a/web/showcase/pdf/features/navigation/table-of-contents.pdf b/web/showcase/pdf/features/navigation/table-of-contents.pdf new file mode 100644 index 00000000..f337a97e Binary files /dev/null and b/web/showcase/pdf/features/navigation/table-of-contents.pdf differ diff --git a/web/showcase/pdf/features/shapes/line-cap.pdf b/web/showcase/pdf/features/shapes/line-cap.pdf new file mode 100644 index 00000000..d54d131e Binary files /dev/null and b/web/showcase/pdf/features/shapes/line-cap.pdf differ diff --git a/web/showcase/pdf/features/shapes/line-fill.pdf b/web/showcase/pdf/features/shapes/line-fill.pdf new file mode 100644 index 00000000..ef5a6b9a Binary files /dev/null and b/web/showcase/pdf/features/shapes/line-fill.pdf differ diff --git a/web/showcase/pdf/features/shapes/photo-clip.pdf b/web/showcase/pdf/features/shapes/photo-clip.pdf new file mode 100644 index 00000000..b8284816 Binary files /dev/null and b/web/showcase/pdf/features/shapes/photo-clip.pdf differ diff --git a/web/showcase/pdf/features/shapes/vector-path.pdf b/web/showcase/pdf/features/shapes/vector-path.pdf new file mode 100644 index 00000000..1e7a872c Binary files /dev/null and b/web/showcase/pdf/features/shapes/vector-path.pdf differ diff --git a/web/showcase/pdf/features/structure/multi-section-document.pdf b/web/showcase/pdf/features/structure/multi-section-document.pdf new file mode 100644 index 00000000..31463572 Binary files /dev/null and b/web/showcase/pdf/features/structure/multi-section-document.pdf differ diff --git a/web/showcase/pdf/features/svg/svg-icon-gallery.pdf b/web/showcase/pdf/features/svg/svg-icon-gallery.pdf new file mode 100644 index 00000000..1254ec17 Binary files /dev/null and b/web/showcase/pdf/features/svg/svg-icon-gallery.pdf differ diff --git a/web/showcase/pdf/features/text/emoji-clip-path.pdf b/web/showcase/pdf/features/text/emoji-clip-path.pdf new file mode 100644 index 00000000..25f0cdc2 Binary files /dev/null and b/web/showcase/pdf/features/text/emoji-clip-path.pdf differ diff --git a/web/showcase/pdf/features/text/emoji-gallery.pdf b/web/showcase/pdf/features/text/emoji-gallery.pdf new file mode 100644 index 00000000..6e2c28a5 Binary files /dev/null and b/web/showcase/pdf/features/text/emoji-gallery.pdf differ diff --git a/web/showcase/pdf/features/text/emoji-shortcodes.pdf b/web/showcase/pdf/features/text/emoji-shortcodes.pdf new file mode 100644 index 00000000..cbeb1465 Binary files /dev/null and b/web/showcase/pdf/features/text/emoji-shortcodes.pdf differ diff --git a/web/showcase/pdf/features/text/emoji-svg-vs-png.pdf b/web/showcase/pdf/features/text/emoji-svg-vs-png.pdf new file mode 100644 index 00000000..330b4780 Binary files /dev/null and b/web/showcase/pdf/features/text/emoji-svg-vs-png.pdf differ diff --git a/web/showcase/pdf/features/text/inline-highlight-chips.pdf b/web/showcase/pdf/features/text/inline-highlight-chips.pdf new file mode 100644 index 00000000..2b0630e5 Binary files /dev/null and b/web/showcase/pdf/features/text/inline-highlight-chips.pdf differ diff --git a/web/showcase/pdf/features/text/inline-shapes.pdf b/web/showcase/pdf/features/text/inline-shapes.pdf new file mode 100644 index 00000000..414f2ddb Binary files /dev/null and b/web/showcase/pdf/features/text/inline-shapes.pdf differ diff --git a/web/showcase/pdf/features/text/inline-svg-icons.pdf b/web/showcase/pdf/features/text/inline-svg-icons.pdf new file mode 100644 index 00000000..1e78ae0f Binary files /dev/null and b/web/showcase/pdf/features/text/inline-svg-icons.pdf differ diff --git a/web/showcase/pdf/features/title/book-template.pdf b/web/showcase/pdf/features/title/book-template.pdf new file mode 100644 index 00000000..4bfbc97e Binary files /dev/null and b/web/showcase/pdf/features/title/book-template.pdf differ diff --git a/web/showcase/pdf/flagships/default/engine-deck.pdf b/web/showcase/pdf/flagships/default/engine-deck.pdf new file mode 100644 index 00000000..e60ebc2a Binary files /dev/null and b/web/showcase/pdf/flagships/default/engine-deck.pdf differ diff --git a/web/showcase/pdf/flagships/default/feature-catalog.pdf b/web/showcase/pdf/flagships/default/feature-catalog.pdf new file mode 100644 index 00000000..9f11b5a4 Binary files /dev/null and b/web/showcase/pdf/flagships/default/feature-catalog.pdf differ diff --git a/web/showcase/pdf/flagships/default/financial-report.pdf b/web/showcase/pdf/flagships/default/financial-report.pdf new file mode 100644 index 00000000..fcde4265 Binary files /dev/null and b/web/showcase/pdf/flagships/default/financial-report.pdf differ