Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ follow semantic versioning; release dates are ISO 8601.
version and link the tag it names. `SECURITY.md`, `SUPPORT.md`, `ROADMAP.md` and
`.github/` are scanned for the first time; historical records are skipped by path,
so a new archived page is covered the day it lands.
- **The package map is derived from the source tree.** A backend was findable only if
someone remembered to list it, and the backend-neutral fixed-layout SPI was missing
from the contributing guide — the one document a reader consults before adding an
output format, where a contributor registers a fragment kind with the backend they
can find, and a kind registered with only one fixed-layout backend renders in one
output and vanishes from the other. Packages are now discovered by scanning every
reactor module for `*Backend` types, so a backend arriving in a new module is covered
the day it lands. Each must be named in the contributing guide and the package map in
its own right: naming a parent covers no child, or adding the missing parent would
have made every package beneath it uncheckable.
- **The READMEs are compiled.** The snippet guard read only `docs/`, leaving the pages
a reader copies from first — the root one and each module's — free to name a method
the library no longer has. Every Java fence in a README now either compiles or carries
the reason it cannot, so an unmarked block no longer reads as a covered one: seven
compile against the current API on every build and forty-five are exempt on the
record. A module README opens with a three-line taste of the API, which the imports it
needs would double in length, so a snippet can take them from the invisible marker
instead; the compile verifies those too. `docs/private/` is out of the scan, and the
two guards that read the published documentation resolve the same set of pages rather
than each keeping its own list.
- **The showcase register is checked against the catalogue.** The register falls back to
a filename-derived card, so an entry keyed on a document the runner never writes is
never read: no card, no warning, no failure. Every entry must now match a generated
document, and its source link must resolve to a file in the tree, so a renamed example
fails the build instead of leaving a 404 behind the card. The example tree is emptied
before it is rebuilt — the runner only writes, and a leftover from an earlier build
would answer for an entry that has nothing left to describe.
- **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
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ The repository uses these normalized package roots:
- `com.demcha.compose.document.node` — semantic node records
- `com.demcha.compose.document.style`, `document.table`, `document.image`, `document.output` — public value types
- `com.demcha.compose.document.layout` — canonical functional layout pipeline
- `com.demcha.compose.document.backend.fixed` — the backend-neutral fixed-layout SPI (`FixedLayoutBackend`, `FixedLayoutBackendProvider`); implement it to add an output format
- `com.demcha.compose.document.backend.fixed.pdf` — PDF fixed-layout backend
- `com.demcha.compose.document.backend.fixed.pptx` — PPTX fixed-layout backend (`@Beta`)
- `com.demcha.compose.document.backend.semantic` — semantic export SPI, the DOCX exporter, and the legacy PPTX manifest
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ The same `DocumentSession` emits both. The PDF backend prints the resolved layou

PowerPoint output needs `graph-compose-render-pptx` on the classpath in addition to `graph-compose`; without it `buildPptx` fails with a `MissingBackendException` naming the artifact. See [Which artifact?](#installation) below.

<!-- doc-example: id=readme-root-one-model-two-outputs mode=method imports=com.demcha.compose.GraphCompose,com.demcha.compose.document.api.DocumentSession,com.demcha.compose.document.api.DocumentPageSize,java.nio.file.Path -->
```java
Path deck = Path.of("twin-output.pptx");
try (DocumentSession doc = GraphCompose.document(Path.of("twin-output.pdf"))
Expand Down Expand Up @@ -158,6 +159,7 @@ pinned to v1.6.5 and earlier but is no longer the documented install option.

## Hello world

<!-- doc-example: id=readme-root-hello-world mode=members -->
```java
import com.demcha.compose.GraphCompose;
import com.demcha.compose.document.api.DocumentPageSize;
Expand Down Expand Up @@ -223,6 +225,7 @@ Three snippets from the vector surfaces. Full runnable versions live in the [exa

**Native chart** &mdash; categories + series in, native vector bars out (no rasterization).

<!-- doc-example-ignore: the surrounding session and its variables are described in the prose above -->
```java
ChartData revenue = ChartData.builder()
.categories("Q1", "Q2", "Q3", "Q4")
Expand All @@ -237,6 +240,7 @@ section.chart(ChartSpec.bar().data(revenue)

**Overshoot-free line** &mdash; a smooth curve constrained to never overshoot the data range.

<!-- doc-example-ignore: the surrounding session and its variables are described in the prose above -->
```java
section.chart(ChartSpec.line().data(series)
.interpolation(LineInterpolation.MONOTONE)
Expand All @@ -245,6 +249,7 @@ section.chart(ChartSpec.line().data(series)

**SVG import + alignment** &mdash; parse SVG to native geometry, seat any fixed node across the width.

<!-- doc-example-ignore: the surrounding session and its variables are described in the prose above -->
```java
SvgIcon globe = SvgIcon.parse(svgMarkup);
flow.addSvgIcon(globe, 48, HorizontalAlign.CENTER);
Expand Down
1 change: 1 addition & 0 deletions core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ resolves a `FontMetricsProvider` through `ServiceLoader`, so a core-only classpa
`MissingBackendException` there — before any render call — and the message names the
artifact to add. With `graph-compose-render-pdf` present the whole path works:

<!-- doc-example: id=readme-core-hello mode=method imports=com.demcha.compose.GraphCompose,com.demcha.compose.document.api.DocumentSession,java.nio.file.Path -->
```java
Path out = Path.of("hello.pdf");
try (DocumentSession doc = GraphCompose.document(out).create()) {
Expand Down
128 changes: 128 additions & 0 deletions core/src/test/java/com/demcha/documentation/PackageMapGuardTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static org.assertj.core.api.Assertions.assertThat;
Expand All @@ -26,6 +29,131 @@ class PackageMapGuardTest {
"components_" + "builders",
"abstract_" + "builders");

/** Documents a contributor consults to find where a backend lives. */
private static final List<String> PACKAGE_ROOT_DOCUMENTS = List.of(
"CONTRIBUTING.md", "docs/architecture/package-map.md");

private static final Pattern REACTOR_MODULE =
Pattern.compile("<module>\\s*([^<]+?)\\s*</module>");

/**
* Every package holding a backend implementation is findable from the documents
* that claim to map the packages.
*
* <p>Derived from the source tree rather than a list: a package qualifies by
* containing a {@code *Backend} type, in any module the reactor builds. That is what
* a contributor is looking for when they ask where a format is implemented, and it is
* how the next backend gets covered without anyone remembering this file. The
* backend-neutral fixed-layout SPI was missing from the contributing guide — the one
* document a reader consults before adding an output format.</p>
*
* <p>Each package must be named in its own right. Accepting an ancestor instead
* looks reasonable and guts the guard: once {@code backend.fixed} appears, every
* {@code backend.fixed.*} is covered for free, including the one that shipped
* undocumented. Matching is boundary-aware and accepts either the fully-qualified
* name or the {@code document.backend.…} tail the docs also use, so
* {@code backend.fixed.pdf} never counts as {@code backend.fixed}.</p>
*/
@Test
void everyBackendPackageIsFindableFromThePackageDocumentation() throws IOException {
Set<String> backendPackages = backendPackages();

assertThat(backendPackages)
.describedAs("no package with a *Backend type was found under document/backend in "
+ "any reactor module — the module list in the root pom or the layout of "
+ "the source roots moved, and this guard is passing vacuously")
.isNotEmpty();

Set<String> missing = new TreeSet<>();
for (String document : PACKAGE_ROOT_DOCUMENTS) {
String text = Files.readString(PROJECT_ROOT.resolve(document));
for (String backendPackage : backendPackages) {
if (!namesPackage(text, backendPackage)
&& !namesPackage(text, shortForm(backendPackage))) {
missing.add(document + " does not name " + backendPackage);
}
}
}

assertThat(missing)
.describedAs("a backend a contributor cannot find in the package map is a backend "
+ "they will not register a handler with — and a fragment kind registered "
+ "with only one fixed-layout backend renders in one output and silently "
+ "vanishes from the other")
.isEmpty();
}

/** Packages under {@code document/backend} that declare a {@code *Backend} type. */
private static Set<String> backendPackages() throws IOException {
Set<String> packages = new TreeSet<>();
for (String moduleRoot : reactorSourceRoots()) {
Path root = PROJECT_ROOT.resolve(moduleRoot);
if (!Files.isDirectory(root)) {
continue;
}
try (var paths = Files.walk(root)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith("Backend.java"))
.map(path -> relativeTo(root, path))
.filter(name -> name.contains("com/demcha/compose/document/backend/"))
.map(name -> name.substring(0, name.lastIndexOf('/')).replace('/', '.'))
.forEach(packages::add);
}
}
return packages;
}

/**
* The main-source root of every module the reactor builds.
*
* <p>Taken from the root {@code pom.xml} rather than listed here, so "a module" means
* what it means to Maven. A list would move the staleness rather than remove it: a
* backend that arrives in a new module is the case this guard exists for, and a
* module missing from a hand-kept list is scanned by nobody and reported by nobody.</p>
*/
private static List<String> reactorSourceRoots() throws IOException {
String pom = Files.readString(PROJECT_ROOT.resolve("pom.xml"));
List<String> roots = new ArrayList<>();
Matcher matcher = REACTOR_MODULE.matcher(pom);
while (matcher.find()) {
roots.add(matcher.group(1) + "/src/main/java");
}
return roots;
}

/** The {@code document.backend.…} tail, which the docs use as often as the full name. */
private static String shortForm(String packageName) {
return packageName.replace("com.demcha.compose.", "");
}

/**
* Whether the text names exactly this package. A trailing identifier character or a
* dot followed by one means the match is really a longer package, so naming
* {@code backend.fixed.pdf} must not count as naming {@code backend.fixed}.
*/
private static boolean namesPackage(String documentText, String packageName) {
int from = 0;
while (true) {
int at = documentText.indexOf(packageName, from);
if (at < 0) {
return false;
}
int after = at + packageName.length();
char next = after < documentText.length() ? documentText.charAt(after) : ' ';
boolean extendsFurther = Character.isJavaIdentifierPart(next)
|| (next == '.' && after + 1 < documentText.length()
&& Character.isJavaIdentifierPart(documentText.charAt(after + 1)));
if (!extendsFurther) {
return true;
}
from = at + 1;
}
}

private static String relativeTo(Path root, Path path) {
return root.relativize(path).toString().replace('\\', '/');
}

@Test
void productionPackagesShouldHavePackageInfo() throws IOException {
Path sourceRoot = PROJECT_ROOT.resolve("core/src/main/java/com/demcha/compose");
Expand Down
1 change: 1 addition & 0 deletions emoji/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ A shortcode resolves to an inline vector glyph inside a rich run, which the flow
any other content. Rendering still needs a backend — `graph-compose-render-pdf`, or the
`graph-compose` wrapper that brings it:

<!-- doc-example: id=readme-emoji-inline mode=method imports=com.demcha.compose.GraphCompose,com.demcha.compose.document.api.DocumentSession,com.demcha.compose.document.dsl.RichText,java.nio.file.Path -->
```java
Path out = Path.of("rated.pdf");
try (DocumentSession doc = GraphCompose.document(out).create()) {
Expand Down
Loading
Loading