diff --git a/CHANGELOG.md b/CHANGELOG.md index c6e062a..01eab42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index e28446c..b40729d 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/assets/readme/manual-dark.pdf b/assets/readme/manual-dark.pdf new file mode 100644 index 0000000..adb8189 Binary files /dev/null and b/assets/readme/manual-dark.pdf differ diff --git a/assets/readme/manual.md b/assets/readme/manual.md index 54a2b85..2a2eaa0 100644 --- a/assets/readme/manual.md +++ b/assets/readme/manual.md @@ -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: diff --git a/assets/readme/manual.pdf b/assets/readme/manual.pdf index 608df1b..1b45580 100644 Binary files a/assets/readme/manual.pdf and b/assets/readme/manual.pdf differ diff --git a/docs/architecture.md b/docs/architecture.md index f557040..ef7ddca 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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. diff --git a/docs/custom-renderers.md b/docs/custom-renderers.md index 60e79b6..7f7f3fa 100644 --- a/docs/custom-renderers.md +++ b/docs/custom-renderers.md @@ -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 @@ -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. diff --git a/docs/theming.md b/docs/theming.md index 9c7f26f..5d4b1c7 100644 --- a/docs/theming.md +++ b/docs/theming.md @@ -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. @@ -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(); +``` diff --git a/src/test/java/io/github/demchaav/markdown/ManualTest.java b/src/test/java/io/github/demchaav/markdown/ManualTest.java index dbb4e89..ef9070a 100644 --- a/src/test/java/io/github/demchaav/markdown/ManualTest.java +++ b/src/test/java/io/github/demchaav/markdown/ManualTest.java @@ -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() @@ -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();