From 32fb86226e22ad03f4853ae6f57e00029ea7ef3e Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Wed, 1 Jul 2026 23:59:37 +0100 Subject: [PATCH] docs: self-rendered user manual (manual.md -> manual.pdf) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit assets/readme/manual.md is a complete user manual — what the library is, install, quick start, the full feature tour (each feature used live in the document), theming, navigation/chrome, extension seams, CLI, examples index — and assets/readme/manual.pdf is that same file rendered BY the library: book-style contents with resolved page numbers, Page-N-of-M footer, auto-opened outline, vector emoji, smart punctuation. The closing section shows the exact composer used. ManualTest renders the committed source with that composer (kept in lockstep via ManualTest.manualComposer()) and asserts the demonstrated features land: outline PageMode, footer on every page, resolved TOC page numbers, em-dash/curly-quote replacements with verbatim code, emoji fallback, footnotes. README Documentation section links both. --- README.md | 4 + assets/readme/manual.md | 261 ++++++++++++++++++ assets/readme/manual.pdf | Bin 0 -> 43214 bytes .../github/demchaav/markdown/ManualTest.java | 90 ++++++ 4 files changed, 355 insertions(+) create mode 100644 assets/readme/manual.md create mode 100644 assets/readme/manual.pdf create mode 100644 src/test/java/io/github/demchaav/markdown/ManualTest.java diff --git a/README.md b/README.md index df612f0..e28446c 100644 --- a/README.md +++ b/README.md @@ -436,6 +436,10 @@ 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 + 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. - **[Architecture](docs/architecture.md)** — the pipeline, the semantic model, and why the parser is decoupled. - **[Theming](docs/theming.md)** — tokens, component styles, deriving themes, packs, diff --git a/assets/readme/manual.md b/assets/readme/manual.md new file mode 100644 index 0000000..54a2b85 --- /dev/null +++ b/assets/readme/manual.md @@ -0,0 +1,261 @@ +--- +title: GraphCompose Markdown +subtitle: User Manual — written in Markdown, rendered by the library itself +author: Artem Demchyshyn +date: 2026-07-01 +--- + +# Contents + +[TOC] + +# What is this? + +**graphcompose-markdown** turns Markdown into themed, paginated PDF. It parses with +[Flexmark](https://github.com/vsch/flexmark-java), maps the parse tree onto an independent +semantic model, and renders that model through the +[GraphCompose](https://github.com/DemchaAV/GraphCompose) engine, which owns measurement, +layout, pagination and output. + +Three concerns stay separate, and that separation is the whole point: + +- **Content** --- your Markdown text. +- **Appearance** --- a theme: colors, fonts, spacing, per-element styles, renderers. +- **Layout** --- the engine: measurement, pagination, PDF. + +The same Markdown can be reskinned into a completely different document without touching +its text. The document you are reading right now is the proof: it is a plain `.md` file +rendered by the library, using the book-style table of contents above (with live page +numbers), the "Page N of M" footer below, vector emoji, and smart punctuation --- every +dash, ellipsis and curly quote in this manual was typed as `--`, `...` and straight quotes. + +> [!NOTE] +> Everything shown in this manual is produced by the code paths it documents. If a feature +> renders here, it works. + +# Installation + +The library is a single Maven dependency; the GraphCompose engine comes in transitively: + +```xml + + io.github.demchaav + graph-compose-markdown + 0.3.0 + +``` + +Two **optional** companion artifacts unlock extras; add them only if you want them: + +| Artifact | What it adds | +|---|---| +| `graph-compose-fonts` | JetBrains Mono for code (`BundledFonts.jetBrainsMonoCode(theme)`) | +| `graph-compose-emoji` | Noto vector emoji: `:rocket:` just works :rocket: | + +Requires Java 17+. + +# Quick start + +```java +import io.github.demchaav.markdown.composer.MarkdownComposer; +import io.github.demchaav.markdown.theme.DefaultMarkdownTheme; +import java.nio.file.Path; + +MarkdownComposer composer = MarkdownComposer.create(DefaultMarkdownTheme.light()); + +composer.render("# Hello\n\nWorld").writePdf(Path.of("hello.pdf")); +composer.renderFile(Path.of("doc.md")).writePdf(Path.of("doc.pdf")); +``` + +A composer is immutable and thread-safe --- build it once, reuse it everywhere. + +## Ways in, ways out + +| Take this in | Call | +|:---|:---| +| A Markdown string | `render(String)` | +| A Markdown file (UTF-8, relative images resolve next to it) | `renderFile(Path)` | +| A file with your own image resolver | `renderFile(Path, ImageResolver)` | +| A parsed Flexmark `Document` | `render(Document)` | +| A hand-built semantic model | `render(MarkdownDocument)` | + +| Get this out | Call | +|:---|:---| +| A PDF file | `writePdf(Path)` | +| A PDF stream | `writePdf(OutputStream)` | +| PDF bytes | `toPdfBytes()` | +| Page images (thumbnails, previews) | `toImages(dpi)` / `toImage(pageIndex, dpi)` | + +# What renders + +Everything in this section is live --- the source of this manual uses each feature. + +## Inline formatting + +Paragraphs carry **bold**, *italic*, ~~strikethrough~~ and `inline code` on its rounded +chip. Links come in three kinds: [external](https://github.com/DemchaAV/graphcompose-markdown), +bare autolinks like https://github.com, and internal jumps --- [back to the contents](#contents) +is a native PDF go-to action, not decoration. Footnotes are clickable in both +directions.[^bidir] + +[^bidir]: Click the marker above to land here; click the `[1]` at the start of this note + to jump back to the citation. + +## Smart punctuation + +This manual is rendered with `smartPunctuation(true)`: dashes -- and --- em-dashes, +ellipses... and "curly quotes" are typed as plain ASCII in the source. It is off by +default (GitHub does not smart-quote); code always stays verbatim: `a --flag "x"`. + +## Lists and tasks + +1. Ordered lists keep their numbering +2. and nest: + - unordered bullets cycle shape by depth + - task lists render real checkboxes: + - [x] parse Markdown + - [x] map to the semantic model + - [ ] profit + +## Tables + +| Feature | Alignment | Status | +|:--------|:---------:|-------:| +| GFM tables | center | :white_check_mark: | +| Per-column alignment | center | :white_check_mark: | +| Inline formatting in cells | **works** | `yes` | + +## Code with syntax highlighting + +```java +// The built-in RegexSyntaxHighlighter covers ~15 languages, no extra dependency. +public static void main(String[] args) { + var composer = MarkdownComposer.create(GitHubTheme.dark()); + byte[] pdf = composer.render("# Reskinned").toPdfBytes(); // 42 ways to say hello +} +``` + +## Quotes, alerts and callouts + +> A blockquote is a left-accented aside. Design tokens drive its cosmetics. + +> [!TIP] +> The five GitHub alerts (`NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`) render as +> color-coded panels with vector icons. + +> [!WARNING] +> Their accent colors are theme tokens (`AlertColors`) --- a dark theme can retune them. + +:::callout success +`:::` custom blocks are an extension seam: register your own renderer per block type, +or let unbound types fall back to this callout style. +::: + +## Emoji + +With `graph-compose-emoji` on the classpath, shortcodes render as crisp vector glyphs at +any size: :rocket: :white_check_mark: :heart: :star: :fire: --- no image files to supply. +Geometric emoji typed literally render as native vector shapes even without it: +status 🔴 blocker, 🟡 important, 🟢 nice to have --- and inside code too: + +``` +Priority: 🔴 blocker 🟡 important 🟢 nice to have +``` + +An unknown shortcode stays readable text: :definitelynotanemoji:. + +# Theming + +A theme has three layers; override exactly as much as you need: + +1. **Design tokens** (`MarkdownTokens`) --- colors, fonts, sizes, spacing, page geometry. +2. **Component styles** (`MarkdownStyles`) --- per-element styles derived from tokens. +3. **Node renderers** (`NodeRenderer`) --- the behaviour turning each node into engine calls. + +```java +MarkdownTheme base = DefaultMarkdownTheme.light(); + +MarkdownTheme custom = MarkdownTheme.builder(base) + .tokens(base.tokens().withColors( + base.tokens().colors().withCodeBackground(DocumentColor.rgb(246, 248, 250)))) + .renderer(CodeBlockNode.class, new MyLabeledCodeBlockRenderer()) + .build(); +``` + +Ready-made packs ship in `io.github.demchaav.markdown.theme.packs`: `GitHubTheme` +(light/dark), `AcademicTheme`, `MinimalTheme`, `BusinessReportTheme` --- plus +`DefaultMarkdownTheme.light()` / `.dark()`. + +# Navigation and page chrome + +Long documents stay navigable; this manual demonstrates all of it: + +- **PDF outline** --- every heading becomes a bookmark; the panel opens automatically + (opt out: `builder().openOutline(false)`). +- **Anchors** --- every heading gets a GitHub-style slug, so `[text](#what-renders)` + jumps [there](#what-renders). +- **`[TOC]`** --- the marker at the top of this manual expands into the contents. Two forms: + the default clickable link list, and the book form used here --- dot leaders and live page + numbers, resolved by the engine after layout: + +```java +MarkdownTheme book = MarkdownTheme.builder(base) + .renderer(TocNode.class, new BookTocRenderer("Contents")) + .tokens(base.tokens().withFooter(FooterTokens.pageNumbers())) + .build(); +``` + +- **Footer** --- the "Page N of M" at the bottom of every page here is `FooterTokens`: + `left`/`center`/`right` templates with `{page}`, `{pages}` and `{date}` placeholders, + disabled by default. + +# Extending + +- **Custom `:::` blocks** --- `builder().customBlock("chart", new ChartRenderer())` routes + every `:::chart` block to your renderer. +- **Swap any renderer** --- `builder().renderer(TableNode.class, myTableRenderer)` replaces + one node type and reuses everything else (that is exactly how the book TOC works). +- **Emoji override** --- an `EmojiResolver` supplying PNG bytes wins over the vector set. +- **Syntax highlighting** --- plug a grammar-based `SyntaxHighlighter` via + `builder().highlighter(...)`; colors stay theme tokens. +- **Strict mode** --- `builder().strictMode(true)` rejects unsupported content (raw HTML) + instead of surfacing it as literal text. + +# Command line + +The standalone `cli/` module wraps all of this for the shell: + +``` +gcmd README.md -t github-dark # theme by name +gcmd docs/guide.md -o out/guide.pdf # explicit output +cat notes.md | gcmd - -o notes.pdf # stdin +``` + +# Runnable examples + +Each feature ships a runnable example under `examples/` --- see `examples/README.md`: +QuickStart, RenderMarkdownFile, ThemeGallery, CustomBlock, AlertsAndOutline, +InPdfNavigation, Toc, **BookToc**, Footer, PngExport, VectorEmoji, Emoji, FrontMatter. + +# How this PDF was made + +This manual is `assets/readme/manual.md`, rendered by `ManualTest` with exactly this +composer --- the self-referential proof that the pipeline works: + +```java +MarkdownTheme base = DefaultMarkdownTheme.light(); +MarkdownTheme manualTheme = MarkdownTheme.builder(base) + .renderer(TocNode.class, new BookTocRenderer()) // page-numbered contents + .tokens(base.tokens().withFooter(FooterTokens.pageNumbers())) + .imageResolver(new DefaultImageResolver(Path.of("assets", "readme"))) + .build(); + +MarkdownComposer.builder() + .theme(manualTheme) + .smartPunctuation(true) // the dashes you saw + .build() + .renderFile(Path.of("assets", "readme", "manual.md")) + .writePdf(Path.of("assets", "readme", "manual.pdf")); +``` + +Both libraries are MIT-licensed. Happy composing. :tada: diff --git a/assets/readme/manual.pdf b/assets/readme/manual.pdf new file mode 100644 index 0000000000000000000000000000000000000000..608df1b55ad33d6110bfee2a2f5e6d2d9cafdf3e GIT binary patch literal 43214 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 UZm;{v3+Ce$0x&YlszCw&1Key~lK=n! literal 0 HcmV?d00001 diff --git a/src/test/java/io/github/demchaav/markdown/ManualTest.java b/src/test/java/io/github/demchaav/markdown/ManualTest.java new file mode 100644 index 0000000..dbb4e89 --- /dev/null +++ b/src/test/java/io/github/demchaav/markdown/ManualTest.java @@ -0,0 +1,90 @@ +package io.github.demchaav.markdown; + +import io.github.demchaav.markdown.composer.MarkdownComposer; +import io.github.demchaav.markdown.extension.DefaultImageResolver; +import io.github.demchaav.markdown.model.TocNode; +import io.github.demchaav.markdown.render.BookTocRenderer; +import io.github.demchaav.markdown.theme.DefaultMarkdownTheme; +import io.github.demchaav.markdown.theme.MarkdownTheme; +import io.github.demchaav.markdown.theme.tokens.FooterTokens; +import org.apache.pdfbox.Loader; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PageMode; +import org.apache.pdfbox.text.PDFTextStripper; +import org.junit.jupiter.api.Test; + +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Renders the committed user manual ({@code assets/readme/manual.md}) with the exact composer + * its final section documents, and asserts the features it demonstrates actually land in the + * output. The committed {@code manual.pdf} is produced from the same source and composer; this + * test keeps the manual honest. + */ +class ManualTest { + + private static final Path BASE = Path.of("assets", "readme"); + + /** The composer the manual's "How this PDF was made" section shows — kept in lockstep. */ + static MarkdownComposer manualComposer() { + MarkdownTheme base = DefaultMarkdownTheme.light(); + MarkdownTheme manualTheme = MarkdownTheme.builder(base) + .renderer(TocNode.class, new BookTocRenderer()) + .tokens(base.tokens().withFooter(FooterTokens.pageNumbers())) + .imageResolver(new DefaultImageResolver(BASE)) + .build(); + return MarkdownComposer.builder() + .theme(manualTheme) + .smartPunctuation(true) + .build(); + } + + @Test + void theManualRendersItsWholeFeatureSet() throws Exception { + byte[] pdf = manualComposer().renderFile(BASE.resolve("manual.md")).toPdfBytes(); + + try (PDDocument doc = Loader.loadPDF(pdf)) { + int pages = doc.getNumberOfPages(); + assertThat(pages).as("a real manual spans pages").isGreaterThan(2); + + // Chrome: the outline panel opens; the footer numbers every page. + assertThat(doc.getDocumentCatalog().getPageMode()).isEqualTo(PageMode.USE_OUTLINES); + String text = new PDFTextStripper().getText(doc); + assertThat(text).contains("Page 1 of " + pages).contains("Page 2 of " + pages); + + // Front matter title block and the major sections. + assertThat(text).contains("GraphCompose Markdown") + .contains("User Manual") + .contains("What is this?") + .contains("How this PDF was made"); + + // Smart punctuation was applied (em-dash present, raw '---' gone from prose)… + assertThat(text).contains("—").contains("“"); + // …while code stayed verbatim. + assertThat(text).contains("--flag"); + + // Emoji: unknown shortcode degrades to readable text. + assertThat(text).contains(":definitelynotanemoji:"); + + // Footnote section rendered. + assertThat(text).contains("Notes"); + } + } + + @Test + void theManualContentsResolvesPageNumbers() throws Exception { + byte[] pdf = manualComposer().renderFile(BASE.resolve("manual.md")).toPdfBytes(); + + try (PDDocument doc = Loader.loadPDF(pdf)) { + PDFTextStripper stripper = new PDFTextStripper(); + stripper.setStartPage(1); + stripper.setEndPage(1); + String tocPage = stripper.getText(doc); + // The book TOC on page 1 lists later sections with resolved page numbers. + assertThat(tocPage).contains("Theming").contains("Command line"); + assertThat(tocPage).containsPattern("[2-9]"); + } + } +}