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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,15 @@ and the project follows [Semantic Versioning](https://semver.org/).
needs the import updated. `BuiltinRenderers` drops from ~750 to ~440 lines.

### Added
- **Self-rendered user manual, in light and dark.** `assets/readme/manual.md` is a complete
guide — install, quick start, a live tour of every feature, theming, navigation/chrome,
extension seams, CLI — rendered **by the library itself** to `manual.pdf` and, from the same
source through `DefaultMarkdownTheme.dark()`, `manual-dark.pdf`: book-style contents with
live page numbers, "Page N of M" footer, auto-opened outline, vector emoji, smart
punctuation. `ManualTest` keeps the committed PDFs, the test composer and the manual's own
"How this PDF was made" snippet in lockstep. The docs guides (`architecture`, `theming`,
`custom-renderers`) now cross-link the manual and cover `FooterTokens` and the
`BookTocRenderer` swap.
- **Opt-in smart punctuation — `MarkdownComposer.builder().smartPunctuation(true)`.** Typographic
replacements at parse time: straight quotes become curly quotes (nested formatting inside the
quotes survives), apostrophes curl (`don't` → `don’t`), `--` becomes an en-dash, `---` an
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -436,7 +436,8 @@ See [examples/README.md](examples/README.md) for the full list.

## Documentation

- **[User manual](assets/readme/manual.md)** → **[manual.pdf](assets/readme/manual.pdf)** — the
- **[User manual](assets/readme/manual.md)** → **[manual.pdf](assets/readme/manual.pdf)**
/ **[manual-dark.pdf](assets/readme/manual-dark.pdf)** (same source, dark theme) — the
full guide, written in Markdown and rendered to PDF *by the library itself*: book-style
contents with live page numbers, "Page N of M" footer, vector emoji, smart punctuation —
every feature it documents is demonstrated on its own pages.
Expand Down
Binary file added assets/readme/manual-dark.pdf
Binary file not shown.
4 changes: 4 additions & 0 deletions assets/readme/manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -258,4 +258,8 @@ MarkdownComposer.builder()
.writePdf(Path.of("assets", "readme", "manual.pdf"));
```

A dark twin --- `manual-dark.pdf` --- is rendered from this **same source file** with
`DefaultMarkdownTheme.dark()`: swap one theme object, reskin the whole document. That is
the separation of content and appearance doing its job.

Both libraries are MIT-licensed. Happy composing. :tada:
Binary file modified assets/readme/manual.pdf
Binary file not shown.
3 changes: 3 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Architecture

> New here? The [user manual](../assets/readme/manual.md) walks the whole feature set in
> one self-rendered document ([PDF](../assets/readme/manual.pdf)).

graphcompose-markdown is a pipeline of small, decoupled stages. Each stage hands the
next a stable data structure, so any stage can be swapped or driven directly.

Expand Down
18 changes: 18 additions & 0 deletions docs/custom-renderers.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Custom renderers

> The [user manual](../assets/readme/manual.md) is rendered with a swapped renderer —
> its page-numbered contents is `BookTocRenderer` replacing the default `[TOC]` renderer.

A `NodeRenderer` decides *how* one kind of semantic node becomes a GraphCompose
document fragment. Override one to restyle a single element; bundle a set into a
`RendererPack` to ship a whole behaviour layer; register one against a `:::` type to
Expand Down Expand Up @@ -160,3 +163,18 @@ concatenation of all token texts must equal the input verbatim (the renderer rel
this to preserve whitespace and indentation).

See also: [architecture.md](architecture.md) · [theming.md](theming.md)

## Swapping a built-in renderer

Every built-in is replaceable per node type. The library ships one such alternative
itself: `BookTocRenderer` turns the `[TOC]` marker into a print-style contents page with
dot leaders and live page numbers, in place of the default clickable link list:

```java
MarkdownTheme book = MarkdownTheme.builder(DefaultMarkdownTheme.light())
.renderer(TocNode.class, new BookTocRenderer("Contents"))
.build();
```

The same seam accepts your own renderer for any node type — tables, code blocks,
headings — while every other component is reused.
19 changes: 19 additions & 0 deletions docs/theming.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Theming

> Want to *see* every theming feature in one rendered document? Open the
> [user manual](../assets/readme/manual.md) and its PDFs —
> [light](../assets/readme/manual.pdf) / [dark](../assets/readme/manual-dark.pdf) are the
> **same Markdown source** through two themes.

A `MarkdownTheme` decides how a document looks. It is built from three layers so you
override exactly what you need and reuse everything else.

Expand Down Expand Up @@ -126,3 +131,17 @@ names, the rich code path is intended for regular-weight code.
local and classpath paths; network fetching is opt-in (implement your own resolver).

See also: [architecture.md](architecture.md) · [custom-renderers.md](custom-renderers.md)

## Page chrome: the running footer

`FooterTokens` is the token group for an optional page footer. It is **disabled by
default**; enable the ready-made "Page N of M" preset, or build a custom one
(`left`/`center`/`right` templates with `{page}`, `{pages}`, `{date}` placeholders,
font size, colour, `showOnFirstPage`):

```java
MarkdownTheme t = DefaultMarkdownTheme.light();
MarkdownTheme numbered = MarkdownTheme.builder(t)
.tokens(t.tokens().withFooter(FooterTokens.pageNumbers()))
.build();
```
29 changes: 27 additions & 2 deletions src/test/java/io/github/demchaav/markdown/ManualTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,20 @@ class ManualTest {

/** The composer the manual's "How this PDF was made" section shows — kept in lockstep. */
static MarkdownComposer manualComposer() {
MarkdownTheme base = DefaultMarkdownTheme.light();
return manualComposer(DefaultMarkdownTheme.light());
}

/** The dark twin: the same document, reskinned — the committed {@code manual-dark.pdf}. */
static MarkdownComposer darkManualComposer() {
return manualComposer(DefaultMarkdownTheme.dark());
}

private static MarkdownComposer manualComposer(MarkdownTheme base) {
MarkdownTheme manualTheme = MarkdownTheme.builder(base)
.renderer(TocNode.class, new BookTocRenderer())
.tokens(base.tokens().withFooter(FooterTokens.pageNumbers()))
.tokens(base.tokens().withFooter(
// The footer picks up the theme's muted ink so it reads on both surfaces.
FooterTokens.pageNumbers().withColor(base.tokens().colors().muted())))
.imageResolver(new DefaultImageResolver(BASE))
.build();
return MarkdownComposer.builder()
Expand Down Expand Up @@ -73,6 +83,21 @@ void theManualRendersItsWholeFeatureSet() throws Exception {
}
}

@Test
void theDarkManualRendersTheSameDocumentReskinned() throws Exception {
byte[] light = manualComposer().renderFile(BASE.resolve("manual.md")).toPdfBytes();
byte[] dark = darkManualComposer().renderFile(BASE.resolve("manual.md")).toPdfBytes();

try (PDDocument lightDoc = Loader.loadPDF(light); PDDocument darkDoc = Loader.loadPDF(dark)) {
// Same content, same structure — only the skin differs.
assertThat(darkDoc.getNumberOfPages()).isEqualTo(lightDoc.getNumberOfPages());
assertThat(darkDoc.getDocumentCatalog().getPageMode()).isEqualTo(PageMode.USE_OUTLINES);
String text = new PDFTextStripper().getText(darkDoc);
assertThat(text).contains("GraphCompose Markdown")
.contains("Page 1 of " + darkDoc.getNumberOfPages());
}
}

@Test
void theManualContentsResolvesPageNumbers() throws Exception {
byte[] pdf = manualComposer().renderFile(BASE.resolve("manual.md")).toPdfBytes();
Expand Down