broken = new TreeMap<>();
+ int checked = 0;
+ for (Path page : pages) {
+ String body = INLINE_CODE.matcher(withoutFencedBlocks(read(page))).replaceAll("");
+ Matcher link = LINK.matcher(body);
+ while (link.find()) {
+ String target = link.group(1);
+ if (target.startsWith("http://") || target.startsWith("https://")
+ || target.startsWith("mailto:") || target.isEmpty()) {
+ continue;
+ }
+
+ checked++;
+ int hash = target.indexOf('#');
+ String filePart = hash < 0 ? target : target.substring(0, hash);
+ String anchor = hash < 0 ? "" : target.substring(hash + 1);
+
+ Path resolved = filePart.isEmpty()
+ ? page.toAbsolutePath().normalize()
+ : page.getParent().resolve(decode(filePart)).toAbsolutePath().normalize();
+
+ if (!filePart.isEmpty() && !Files.exists(resolved)) {
+ broken.put(relative(page) + " -> " + target, "no such file");
+ continue;
+ }
+ // Only markdown has anchors this can compute; a link into a PDF or a
+ // source file with a fragment is not something to judge here.
+ if (!anchor.isEmpty() && anchors.containsKey(resolved)
+ && !anchors.get(resolved).contains(decode(anchor))) {
+ broken.put(relative(page) + " -> " + target,
+ "no heading in " + relative(resolved) + " anchors there");
+ }
+ }
+ }
+
+ assertThat(broken)
+ .describedAs("a relative link that goes nowhere. Renaming a heading breaks every "
+ + "jump to it and moving a file breaks every link into it — both look "
+ + "harmless in the diff, and neither is visible without following the "
+ + "link. Fix the link or restore what it pointed at")
+ .isEmpty();
+
+ // "No broken links" and "no links read" are the same shade of green. A regex
+ // that stops matching — a link syntax nobody anticipated, a scan root that
+ // moved — would leave this passing over nothing at all, which is the state
+ // the documentation was already in before this guard existed.
+ assertThat(checked)
+ .describedAs("only %d relative links were examined across %d pages; the scan is "
+ + "reading far less than this repository links, so a green result here "
+ + "means the matching broke rather than the links being sound",
+ checked, pages.size())
+ .isGreaterThan(300);
+ }
+
+ /**
+ * The anchors GitHub generates for a page.
+ *
+ * The rule: take the heading text, drop HTML tags, keep a link's text rather
+ * than its target, lowercase, remove everything that is not a word character,
+ * whitespace or a hyphen, then replace whitespace with hyphens. Leading and
+ * trailing hyphens survive — {@code ## 🚀 Start here} anchors as
+ * {@code -start-here}, not {@code start-here} — and a repeated heading gets
+ * {@code -1}, {@code -2} appended.
+ *
+ * @param page the markdown file
+ * @return every anchor a link in this repository could legitimately target
+ * @throws IOException when the page cannot be read
+ */
+ private static Set anchorsOf(Path page) throws IOException {
+ String body = withoutFencedBlocks(read(page));
+ Set anchors = new LinkedHashSet<>();
+ Map seen = new HashMap<>();
+
+ Matcher heading = HEADING.matcher(body);
+ while (heading.find()) {
+ String slug = slug(heading.group(1));
+ int occurrence = seen.merge(slug, 1, Integer::sum) - 1;
+ anchors.add(occurrence == 0 ? slug : slug + "-" + occurrence);
+ }
+
+ Matcher explicit = EXPLICIT_ANCHOR.matcher(body);
+ while (explicit.find()) {
+ anchors.add(explicit.group(1));
+ }
+ return anchors;
+ }
+
+ /** GitHub's heading-to-anchor rule. */
+ private static String slug(String heading) {
+ String text = HTML_TAG.matcher(heading.trim()).replaceAll("");
+ text = HEADING_LINK.matcher(text).replaceAll("$1");
+ text = text.replace("`", "").toLowerCase(java.util.Locale.ROOT);
+ text = NOT_IN_ANCHOR.matcher(text).replaceAll("");
+ return text.replaceAll("\\s", "-");
+ }
+
+ private static String withoutFencedBlocks(String markdown) {
+ return FENCED_BLOCK.matcher(markdown).replaceAll("");
+ }
+
+ /** {@code %20} and friends, so an encoded path is compared as the file is named. */
+ private static String decode(String target) {
+ return java.net.URLDecoder.decode(target, StandardCharsets.UTF_8);
+ }
+
+ /**
+ * The markdown this repository publishes: the documentation tree, the root
+ * pages, and every module README. {@code docs/private} is working material,
+ * ignored by git and not published, so it is not held to this.
+ */
+ private static List documentationPages() throws IOException {
+ List pages = new ArrayList<>();
+ Path docs = PROJECT_ROOT.resolve("docs");
+ if (Files.isDirectory(docs)) {
+ try (Stream tree = Files.walk(docs)) {
+ tree.filter(p -> p.toString().endsWith(".md"))
+ .filter(p -> !p.toAbsolutePath().normalize().startsWith(
+ docs.resolve("private").toAbsolutePath().normalize()))
+ .forEach(pages::add);
+ }
+ }
+ for (String root : new String[]{"README.md", "CONTRIBUTING.md", "SUPPORT.md", "SECURITY.md"}) {
+ Path page = PROJECT_ROOT.resolve(root);
+ if (Files.exists(page)) {
+ pages.add(page);
+ }
+ }
+ try (Stream modules = Files.list(PROJECT_ROOT)) {
+ modules.filter(Files::isDirectory)
+ .map(module -> module.resolve("README.md"))
+ .filter(Files::exists)
+ .forEach(pages::add);
+ }
+ return pages;
+ }
+
+ private static String read(Path page) throws IOException {
+ return Files.readString(page, StandardCharsets.UTF_8).replace("\r\n", "\n");
+ }
+
+ private static String relative(Path path) {
+ return PROJECT_ROOT.relativize(path.toAbsolutePath().normalize())
+ .toString().replace('\\', '/');
+ }
+}
diff --git a/examples/README.md b/examples/README.md
index 76c8fc66..7b72cddc 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -91,8 +91,8 @@ 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-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) |
+| [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) |
| **Engine Showcase** | Single-page cinematic brand promo — semantic-graph → polished-PDFs visual metaphor with rounded clip frame, magazine headline lockup, KPI cards, capability columns; source of the README hero image | [Source](src/main/java/com/demcha/examples/flagships/EngineShowcase.java) |
@@ -110,10 +110,10 @@ are with the canonical DSL, then jump to its detailed section below.
| [Inline SVG icons](#inline-svg-icons) | `RichText.svgIcon(icon, size)` — a parsed multi-colour `SvgIcon` on the text baseline, crisp at any zoom and carrying its own colours | [PDF](../assets/readme/examples/inline-svg-icons.pdf) · [Source](src/main/java/com/demcha/examples/features/text/InlineSvgIconExample.java) |
| [Colour emoji](#colour-emoji) | `RichText.emoji(":star:", size)` — GitHub-style shortcodes resolve to inline vector glyphs via the `graph-compose-emoji` artifact; unknown codes fall back to literal text | [PDF](../assets/readme/examples/emoji-shortcodes.pdf) · [Source](src/main/java/com/demcha/examples/features/text/EmojiShortcodeExample.java) |
| [Section presets](#section-presets) | `pageBackground`, `band`, `softPanel`, `accentLeft / Right / Top / Bottom`, per-corner `DocumentCornerRadius` | [PDF](../assets/readme/examples/section-presets.pdf) · [Source](src/main/java/com/demcha/examples/features/text/SectionPresetsExample.java) |
-| [Nested lists](#nested-lists-v16) | `ListBuilder.addItem(label, Consumer)` — depth cascade, per-depth markers, mixed flat / nested authoring | [PDF](../assets/readme/examples/nested-list-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/lists/NestedListExample.java) |
-| [Composed table cells](#composed-table-cells-v16) | `DocumentTableCell.node(DocumentNode)` — paragraphs, lists, sub-tables inside cells with two-pass measurement | [PDF](../assets/readme/examples/composed-table-cell-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java) |
+| Nested lists | `ListBuilder.addItem(label, Consumer)` — depth cascade, per-depth markers, mixed flat / nested authoring | [PDF](../assets/readme/examples/nested-list-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/lists/NestedListExample.java) |
+| Composed table cells | `DocumentTableCell.node(DocumentNode)` — paragraphs, lists, sub-tables inside cells with two-pass measurement | [PDF](../assets/readme/examples/composed-table-cell-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/ComposedTableCellExample.java) |
| [Inline-code column wrap](#inline-code-column-wrap) | A long `inlineCode(...)` coordinate breaks at its `. : / -` seams inside a narrow **fixed** column and an **auto** column grows to fit it on one line | [PDF](../assets/readme/examples/inline-code-column-wrap.pdf) · [Source](src/main/java/com/demcha/examples/features/tables/InlineCodeColumnWrapExample.java) |
-| [Canvas layer (free placement)](#canvas-layer-v16) | `CanvasLayerNode` — pixel-precise `(x, y)` placement of children inside a fixed bounding box, with `ClipPolicy` clipping | [PDF](../assets/readme/examples/canvas-layer-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/canvas/CanvasLayerExample.java) |
+| Canvas layer (free placement) | `CanvasLayerNode` — pixel-precise `(x, y)` placement of children inside a fixed bounding box, with `ClipPolicy` clipping | [PDF](../assets/readme/examples/canvas-layer-showcase.pdf) · [Source](src/main/java/com/demcha/examples/features/canvas/CanvasLayerExample.java) |
| [Transforms](#transforms) | `rotate`, `scale`, and per-layer `zIndex` swap | [PDF](../assets/readme/examples/transforms.pdf) · [Source](src/main/java/com/demcha/examples/features/transforms/TransformsExample.java) |
| [Block alignment](#block-alignment) | `addAligned(align, node)` / `addSvgIcon(icon, w, align)` — seat any fixed-size node left / centre / right across the content width | [PDF](../assets/readme/examples/block-align.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BlockAlignExample.java) |
| [Content bleed](#content-bleed) | `band.bleedToEdge(TOP, LEFT, RIGHT)` / `bleed(DocumentBleed.of(...))` — a section's fill reaches the trimmed page edge while its children stay in the content margin | [PDF](../assets/readme/examples/content-bleed.pdf) · [Source](src/main/java/com/demcha/examples/features/layout/BleedExample.java) |
@@ -126,9 +126,9 @@ are with the canonical DSL, then jump to its detailed section below.
| Example | What it shows | Preview · Source |
|---|---|---|
-| [CV — template gallery](#cv-template-gallery) | The v2 CV presets in one orchestrated run | [Source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java) |
-| [Cover letter — template gallery](#cover-letter-template-gallery) | All paired v2 cover-letter presets in one orchestrated run | [Source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java) |
-| [Proposal — cinematic V2](#proposal-cinematic-v2) | `ModernProposal + BrandTheme.proposalModern()` | [PDF](../assets/readme/examples/proposal-cinematic.pdf) · [Source](src/main/java/com/demcha/examples/templates/proposal/ProposalCinematicFileExample.java) |
+| [CV — template gallery](#cv--template-gallery) | The v2 CV presets in one orchestrated run | [Source](src/main/java/com/demcha/examples/templates/cv/CvTemplateGalleryFileExample.java) |
+| [Cover letter — template gallery](#cover-letter--template-gallery) | All paired v2 cover-letter presets in one orchestrated run | [Source](src/main/java/com/demcha/examples/templates/coverletter/CoverLetterTemplateGalleryFileExample.java) |
+| [Proposal — cinematic V2](#proposal--cinematic-v2) | `ModernProposal + BrandTheme.proposalModern()` | [PDF](../assets/readme/examples/proposal-cinematic.pdf) · [Source](src/main/java/com/demcha/examples/templates/proposal/ProposalCinematicFileExample.java) |
### 🔧 Advanced SPI