Skip to content
Merged
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,73 @@ All notable changes to **graph-compose-markdown** are documented here.
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/),
and the project follows [Semantic Versioning](https://semver.org/).

## v0.4.0 — 2026-08-05

Minor release: headings stop being stranded at the bottom of a page, and the PDF backend
finally draws the link underlines and strikethrough every theme was already asking for.
Both come from moving to the GraphCompose 2.x engine line (2.1.1). The only public-API
change is an added `PageTokens` component, which keeps a constructor overload, so existing
theme code compiles unchanged.

### Upgrading — expect your documents to repaginate

Both fixes are on by default, and the heading one moves content. Any document where a heading
sat at the bottom of a page now has that heading — and everything after it — one position
further down. This is not a rare shape: rendering this repository's own six-page manual, two
headings were stranded (`Ways in, ways out` and `Tables`), and fixing them redrew four of the
six pages by 22–39 % of their pixels. The page *count* did not change there, but it can.

If you compare rendered PDFs against committed baselines, regenerate them with this release.
To keep the old flow, build `PageTokens` with the fourth argument `false`; to keep links
un-underlined, set `ShapeTokens.underlineLinks` to `false`.

### Public API
- **A heading is no longer stranded at the bottom of a page, apart from the content it
introduces.** `HeadingRenderer` now wraps its paragraph in a one-child section marked
`SectionBuilder.keepWithNext()` (the engine's opt-in orphan rule, new in GraphCompose
2.1.0), so a heading that fits at a page bottom but whose body's first line does not
moves down to join it. The flag has to sit on a section because it is a node-level
pagination property and a paragraph cannot carry one; the bookmark and the anchor stay
on the paragraph, so outline entries and `#slug` link destinations resolve to the
heading's own position exactly as before. Documents with no orphan render unchanged.
- **`PageTokens` gains `keepHeadingWithNext`** (default `true`), so a theme can restore the
plain flow. The previous three-argument constructor is kept as an overload that defaults
the flag on, so existing theme code compiles and behaves identically apart from the fix.

This covers every block kind, including the five this library marks `keepTogether()` — a
code block, a blockquote, a GitHub alert, a custom callout and an unsupported-block panel.
Those needed the engine fix that shipped in GraphCompose 2.1.1: on 2.1.0 the lookahead
measured the first *line* of the following block, but a keep-together block has no line to
break after — it relocates entire — so the heading was left behind. Rendering
`samples/kitchen-sink.md` from the `gp-markdown-view` viewer, the one orphan across all its
samples (`## Blockquote`) survives the wrapping alone and disappears on 2.1.1.

### Build
- **GraphCompose engine bumped 2.0.0 → 2.1.1.** Brings `SectionBuilder.keepWithNext()` (used
above), the 2.1.1 fix that lets its lookahead see `keepTogether()` — without which the
heading rule misses every panel-shaped block this library builds — and two rendering fixes
this library benefits from with no source change: the PDF
backend now actually draws `UNDERLINE` and `STRIKETHROUGH`, so the link underlines every theme
already asks for through `ShapeTokens.underlineLinks` and `~~strikethrough~~` stop rendering as
plain text, and `DocumentColor.rgba` alpha is honoured on text, lines, borders and table fills.
Metrics are unchanged — the marks are painted at measured positions — so nothing reflows.
PDFBox moves 3.0.7 → 3.0.8 transitively. 193 library tests pass on 2.1.1.
- **GraphCompose engine bumped 1.9.1 → 2.0.0.** The 2.0 release splits the monolithic engine
into per-concern modules, but `io.github.demchaav:graph-compose` stays the drop-in default —
it is now a thin aggregator over `graph-compose-core` + `graph-compose-render-pdf`, so this
library keeps rendering PDF with **no dependency change and no change to `MarkdownComposer`
or any public type**. The optional `graph-compose-fonts` / `graph-compose-emoji` add-ons keep
their own version lines and are unchanged. The only source touched was a test: 2.0 removed the
deprecated `linkOptions()` read-accessor on the engine's inline runs, so `InlineRendererTest`
now reads the surrounding link through `linkTarget()` / `ExternalLinkTarget.options().uri()`
(the value it asserts is unchanged). 188 library tests + the CLI suite pass on 2.0.0.
- **GraphCompose engine bumped 1.9.0 → 1.9.1.** Long inline-code tokens with no spaces
(package coordinates, fully-qualified class names, URLs) now break *within* their table
cell or paragraph — at `.` `:` `/` `-` seams, char-splitting only as a last resort — instead
of overflowing the neighbouring column or the right page margin; `auto()` engine table
columns also grow to fit composed cell content. No public API change. (Also shipped as the
`0.3.1` patch on the 0.3.x line.)

## v0.3.1 — 2026-07-06

Patch release: picks up the GraphCompose 1.9.1 engine, which stops long inline-code from
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Markdown ──Flexmark──▶ Flexmark AST ──mapper──▶ Semantic mod
types downstream)
```

> Status: `0.3.1` released. The API may still change before `1.0.0`.
> Status: `0.4.0` released. The API may still change before `1.0.0`.

## Showcase

Expand Down Expand Up @@ -83,7 +83,7 @@ Maven (once released):
<dependency>
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-markdown</artifactId>
<version>0.3.1</version>
<version>0.4.0</version>
</dependency>
```

Expand All @@ -100,7 +100,7 @@ import java.nio.file.Path;
String md = """
# Release notes

GraphCompose **1.9** ships *themeable* Markdown rendering.
GraphCompose **2.0** ships *themeable* Markdown rendering.

- Headings, lists and `inline code`
- Syntax-highlighted code blocks
Expand Down
Binary file modified assets/readme/manual-dark.pdf
Binary file not shown.
Binary file modified assets/readme/manual.pdf
Binary file not shown.
4 changes: 2 additions & 2 deletions cli/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
-->
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-markdown-cli</artifactId>
<version>0.3.1</version>
<version>0.4.0</version>
<packaging>jar</packaging>

<name>graph-compose-markdown CLI</name>
Expand All @@ -28,7 +28,7 @@
<properties>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gcmd.version>0.3.1</gcmd.version>
<gcmd.version>0.4.0</gcmd.version>
<picocli.version>4.7.6</picocli.version>
<junit.bom.version>6.1.1</junit.bom.version>
<assertj.version>3.27.7</assertj.version>
Expand Down
12 changes: 11 additions & 1 deletion docs/theming.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Pure cosmetic values, grouped:
| `TypographyTokens` | body / heading / code font families, body & code sizes, line spacing, the six heading sizes |
| `SpacingTokens` | block gaps, paddings, table cell padding |
| `ShapeTokens` | corner radii, border/line weights |
| `PageTokens` | page size, margins, content width |
| `PageTokens` | page size, margins, content width, `keepHeadingWithNext` |
| `SyntaxColors` | code highlight colors (keyword, string, comment, number, annotation, function) |

Swap a token group to reskin everything that derives from it. Tokens are immutable
Expand All @@ -38,6 +38,16 @@ MarkdownTokens tokens = base.tokens()
.withSyntax(SyntaxColors.defaultDark());
```

`PageTokens.keepHeadingWithNext` (default `true`) is the one non-cosmetic token: it
decides whether a heading may be left stranded as the last block on a page, apart
from the content it introduces. Set it to `false` for the plain flow:

```java
PageTokens page = base.tokens().page();
MarkdownTokens tokens = base.tokens().withPage(
new PageTokens(page.pageSize(), page.margin(), page.contentWidth(), false));
```

## Layer 2 — component styles (`MarkdownStyles`)

`MarkdownStyles` derives per-element styles (`CodeBlockStyle`, `QuoteStyle`,
Expand Down
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ From the repository root:
./mvnw -B -ntp -DskipTests install
```

This installs `io.github.demchaav:graph-compose-markdown:0.3.1` into your
This installs `io.github.demchaav:graph-compose-markdown:0.4.0` into your
local Maven repository, which these examples depend on.

## 2. Run an example
Expand Down
4 changes: 2 additions & 2 deletions examples/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
-->
<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-markdown-examples</artifactId>
<version>0.3.1</version>
<version>0.4.0</version>
<packaging>jar</packaging>

<name>graph-compose-markdown examples</name>
Expand All @@ -26,7 +26,7 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<!-- Track the library version from one place (mirrors cli/pom.xml's gcmd.version)
so the detached examples pom cannot drift behind the next snapshot bump. -->
<gcmd.version>0.3.1</gcmd.version>
<gcmd.version>0.4.0</gcmd.version>
<!-- Examples are never published. -->
<maven.deploy.skip>true</maven.deploy.skip>
<maven.install.skip>true</maven.install.skip>
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.demchaav</groupId>
<artifactId>graph-compose-markdown</artifactId>
<version>0.3.1</version>
<version>0.4.0</version>

<name>GraphCompose Markdown</name>
<description>A themeable Markdown document composer powered by the GraphCompose layout engine.</description>
Expand Down Expand Up @@ -46,7 +46,7 @@
<maven.compiler.release>17</maven.compiler.release>

<!-- Runtime / library dependencies -->
<graphcompose.version>1.9.1</graphcompose.version>
<graphcompose.version>2.1.1</graphcompose.version>
<graphcompose.fonts.version>1.0.0</graphcompose.fonts.version>
<graphcompose.emoji.version>1.0.0</graphcompose.emoji.version>
<flexmark.version>0.64.8</flexmark.version>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.demchaav.markdown.render;

import com.demcha.compose.document.dsl.ParagraphBuilder;
import com.demcha.compose.document.dsl.RichText;
import com.demcha.compose.document.dsl.SectionBuilder;
import com.demcha.compose.document.image.DocumentImageFitMode;
Expand Down Expand Up @@ -29,10 +30,12 @@
import io.github.demchaav.markdown.theme.style.InlineStyle;
import io.github.demchaav.markdown.theme.style.MarkdownStyles;
import io.github.demchaav.markdown.theme.tokens.AlertColors;
import io.github.demchaav.markdown.theme.tokens.PageTokens;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

/**
* The default {@link NodeRenderer} registration plus the smaller built-in renderers.
Expand Down Expand Up @@ -76,6 +79,15 @@ public static void registerDefaults(RendererRegistry registry) {
* bookmark (outline entry) at its level so the rendered document gets a navigable
* heading tree in the viewer's outline pane, and declares a GitHub-style anchor so
* {@code [text](#heading)} links can jump to it.
*
* <p>Unless the theme turns {@link PageTokens#keepHeadingWithNext()} off, the
* paragraph is wrapped in a one-child section marked
* {@link SectionBuilder#keepWithNext()}, so a heading that would otherwise be
* stranded at the bottom of a page — with nothing or a single line of its body
* under it — moves down to join the block it introduces. The flag lives on the
* section because it is a node-level pagination property and a paragraph cannot
* carry it; the bookmark and the anchor stay on the paragraph, so outline and
* link destinations resolve to the heading's own position exactly as before.</p>
*/
public static final class HeadingRenderer implements NodeRenderer<HeadingNode> {
@Override
Expand All @@ -88,12 +100,17 @@ public void render(HeadingNode node, SectionBuilder host, RenderContext ctx) {
// falling back to an on-the-fly slug if this heading was somehow not planned.
String planned = ctx.headingSlug(node);
String anchor = planned != null ? planned : ctx.headingAnchor(title);
host.addParagraph(p -> {
Consumer<ParagraphBuilder> paragraph = p -> {
p.rich(rich).margin(new DocumentInsets(above, 0, 0, 0)).anchor(anchor);
if (!title.isEmpty()) {
p.bookmark(new DocumentBookmarkOptions(title, node.level()));
}
});
};
if (ctx.tokens().page().keepHeadingWithNext()) {
host.addSection(s -> s.keepWithNext().addParagraph(paragraph));
} else {
host.addParagraph(paragraph);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,31 @@
import java.util.Objects;

/**
* Page geometry tokens.
* Page geometry and pagination tokens.
*
* @param pageSize the page size
* @param margin the page margin (top, right, bottom, left)
* @param contentWidth the usable content width in points (page width minus left/right margin)
* @param pageSize the page size
* @param margin the page margin (top, right, bottom, left)
* @param contentWidth the usable content width in points (page width minus left/right margin)
* @param keepHeadingWithNext whether a heading is kept with the block it introduces across a page
* break (a theme can set {@code false} to restore the plain flow)
*/
public record PageTokens(DocumentPageSize pageSize, DocumentInsets margin, double contentWidth) {
public record PageTokens(DocumentPageSize pageSize, DocumentInsets margin, double contentWidth,
boolean keepHeadingWithNext) {

/** Validates the page size and margin are present. */
public PageTokens {
Objects.requireNonNull(pageSize, "pageSize");
Objects.requireNonNull(margin, "margin");
}

/**
* Creates page tokens that keep headings with their content (the default).
*
* @param pageSize the page size
* @param margin the page margin (top, right, bottom, left)
* @param contentWidth the usable content width in points
*/
public PageTokens(DocumentPageSize pageSize, DocumentInsets margin, double contentWidth) {
this(pageSize, margin, contentWidth, true);
}
}
Loading