diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index fd43bf0..f9d89c9 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Install dependencies diff --git a/docs/AUDIT.md b/docs/AUDIT.md index d146d41..eef9580 100644 --- a/docs/AUDIT.md +++ b/docs/AUDIT.md @@ -176,6 +176,7 @@ | 2 | `core/src/orchestrator.ts` private methods not line-by-line documented | Low | Covered in core-api.md at algorithm level; technical/core.md covers private methods | | 3 | Test suite reference is incomplete | Info | Package-level parser, renderer, core, and CLI tests now exist | | 4 | `renderer-markdown` private method line numbers not exact | Low | Method names and algorithms are accurate | +| 5 | `parser-java`, `renderer-html` and `renderer-pdf` are not in the file-level tables above | Low | Technical references now exist for them ([parser-java](technical/parser-java.md), [renderer-html](technical/renderer-html.md), [renderer-pdf](technical/renderer-pdf.md)). The inventory and coverage tables were not regenerated, so the 100% figure still describes only the original 22 source files | ### No Critical Gaps diff --git a/docs/README.md b/docs/README.md index 0a47b06..0ed138f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,9 @@ > Complete technical documentation for the DocGen universal documentation generator. -**Total:** 17 documents | 15,355+ lines | 100% source coverage +**Total:** 20 documents | 15,355+ lines | see [AUDIT.md](AUDIT.md) for what the coverage figure covers + + --- @@ -19,8 +21,10 @@ |----------|---------|-------| | [Core](technical/core.md) | `@docgen/core` | 2,283 | | [TypeScript Parser](technical/parser-typescript.md) | `@docgen/parser-typescript` | 1,301 | -| Java Parser | `@docgen/parser-java` | Shipped | +| [Java Parser](technical/parser-java.md) | `@docgen/parser-java` | 248 | | [Markdown Renderer](technical/renderer-markdown.md) | `@docgen/renderer-markdown` | 1,305 | +| [HTML Renderer](technical/renderer-html.md) | `@docgen/renderer-html` | 177 | +| [PDF Renderer](technical/renderer-pdf.md) | `@docgen/renderer-pdf` | 178 | | [CLI](technical/cli.md) | `@docgen/cli` | 778 | ### Specifications & References (Phase 2) @@ -65,7 +69,10 @@ docs/ ├── technical/ │ ├── core.md ← @docgen/core deep reference │ ├── parser-typescript.md ← @docgen/parser-typescript reference +│ ├── parser-java.md ← @docgen/parser-java reference │ ├── renderer-markdown.md ← @docgen/renderer-markdown reference +│ ├── renderer-html.md ← @docgen/renderer-html reference +│ ├── renderer-pdf.md ← @docgen/renderer-pdf reference │ └── cli.md ← @docgen/cli reference ├── specifications/ │ └── docir-spec.md ← DocIR type specification diff --git a/docs/technical/parser-java.md b/docs/technical/parser-java.md new file mode 100644 index 0000000..ab4875a --- /dev/null +++ b/docs/technical/parser-java.md @@ -0,0 +1,248 @@ +# @docgen/parser-java -- Technical Reference + + + +> **Evidence.** Written from `packages/parser-java/src/index.ts` and its test at RepoScribe commit `4cbc9e4` (2026-08-11). Statements marked *(by code reading)* are not covered by a test. The test suite was not re-run when this document was written. + +## 1. Overview + +`@docgen/parser-java` is the Java and Spring Boot parser plugin. It reads `.java` source files, builds a DocIR (Document Intermediate Representation) `ModuleNode` for every top-level class, interface and enum, and extracts Javadoc, annotations and Spring MVC endpoint metadata. It uses the Java grammar from `tree-sitter-wasms`, loaded through `web-tree-sitter`, so no JDK is needed to parse. + +**Where it fits:** the parse layer of the pipeline. + +``` +Java source --> [@docgen/parser-java] --> DocIR --> [Transformers] --> DocIR --> [Renderers] --> Output Files + ^^^^^^^^^^^^^^^^^^^^^ +``` + +- **npm package:** `@docgen/parser-java` v1.1.0 +- **Dependencies:** `@docgen/core` (workspace), `tree-sitter-wasms` ^0.1.13, `web-tree-sitter` 0.20.8 +- **Source file:** `packages/parser-java/src/index.ts` (494 lines, single file) +- **Export:** `JavaParser` class (named and default export) +- **Plugin type:** `parser`; `language = "java"`; `supports = ["java"]` + +--- + +## 2. Installation & Setup + +The package is part of the RepoScribe monorepo. Add a `java` entry under `languages` in `.docgen.yaml`: + +```yaml +languages: + - name: java + source: src/main/java + include: ["**/*.java"] + exclude: ["**/test/**", "**/generated/**"] + parser: "@docgen/parser-java" +``` + +The orchestrator loads every parser named in `languages[].parser`. The end-to-end fixture `fixtures/e2e/.docgen.yaml` combines a TypeScript and a Java source tree in one run. + +Programmatic use (this mirrors the package test): + +```typescript +import { JavaParser } from "@docgen/parser-java"; + +const parser = new JavaParser(); +await parser.initialize({ + projectConfig: resolvedConfig, + workDir: "/path/to/repo", + options: {}, + logger: console, +}); + +const docir = await parser.parse(["/path/to/repo/src/main/java/com/example/UserController.java"], {}); +``` + +**Requirements:** Node.js 20 or later. `initialize` resolves `tree-sitter-wasms/out/tree-sitter-java.wasm` with `require.resolve`, so the dependency must be installed. + +--- + +## 3. Architecture + +### Single-file design + +The whole parser is one class, `JavaParser`, with the standard plugin lifecycle (`initialize`, `validate`, `parse`, `cleanup`) and a set of private helpers. There is no template engine or code generation. + +### Parse flow + +```mermaid +graph TD + A["initialize(config)"] --> B["Parser.init() + load java.wasm"] + C["parse(files)"] --> D["read file, tree-sitter parse"] + D --> E["extractPackage + imports"] + D --> F["top-level class / interface / enum"] + F --> G["parseTypeDeclaration"] + G --> H["parseMember: method, constructor, field, enum constant"] + H --> I["extractEndpoint (Spring mapping annotations)"] + G --> J["ModuleNode"] + J --> K["DocIR.modules"] +``` + +`parse` calls `initialize` itself if the parser was not initialised. The language configuration argument (`_langConfig`) is not used; file selection is done by the orchestrator before `parse` is called. + +### Scope of what is parsed + +Only these node types directly under the compilation unit become modules: `class_declaration`, `interface_declaration`, `enum_declaration`. Inside a type body only `method_declaration`, `constructor_declaration`, `field_declaration` and `enum_constant` become members. + +--- + +## 4. Public API Reference + +### JavaParser class + +```typescript +export class JavaParser implements ParserPlugin { + readonly name = "@docgen/parser-java"; + readonly version = "1.1.0"; + readonly type = "parser"; + readonly language = "java"; + readonly supports = ["java"]; + + async initialize(config: PluginConfig): Promise; + async validate(): Promise; // always { valid: true, errors: [], warnings: [] } + async parse(files: string[], _langConfig: unknown): Promise; + async cleanup(): Promise; // no-op +} +``` + +`parse` returns a DocIR whose `modules` hold the result. Its metadata is a placeholder (`name: "unnamed"`, `version: "0.0.0"`). The orchestrator copies only `modules`, `adrs` and `changelog` from each parser result into its own DocIR, so the placeholder does not reach the output. + +### Module mapping (`ModuleNode`) + +| Field | Value | +|---|---| +| `id` | `.`, or `` when there is no package | +| `name` | Type name (`"Anonymous"` if the grammar yields none) | +| `filePath` | Path relative to `workDir` | +| `kind` | `class`, `interface` or `enum` | +| `description`, `tags` | From the Javadoc block directly before the type | +| `members` | See member mapping below | +| `dependencies` | One `import` entry per import statement, plus an `inheritance` entry for the superclass | +| `decorators` | Class annotations (name and raw text) | +| `typeParameters` | Generic parameters with an optional `extends` constraint | +| `extends`, `implements` | Superclass text, and the `implements` list when present | +| `exports` | Always `{ isDefault: false, isNamed: true, exportedName: }` | +| `sourceFacts.fileRole` | `route` for `@RestController` / `@Controller`; `service` for `@Service` / `@Repository`; otherwise `unknown` | +| `sourceFacts.usesServiceDependencies` | `true` for `@Service` / `@Repository` | + +Recognised stereotype annotations: `RestController`, `Controller`, `Service`, `Repository`, `Component`. `@Component` is detected but maps to `fileRole: "unknown"`. + +### Member mapping (`MemberNode`) + +| Source construct | `kind` | Notes | +|---|---|---| +| Method | `method` | `isAbstract` is true when there is no body (interface methods) | +| Constructor | `constructor` | `returnType` is `null`; no endpoint extraction | +| Field | `field` | One member per declarator; `signature` is the declaration without the trailing `;` | +| Enum constant | `enum-member` | `returnType` is `null` | + +Other member fields: + +- `visibility`: `private`, `protected` or `public` from the modifiers; anything else (package-private, or an interface method with no modifier) is `internal`. +- `isStatic` and `isAbstract` come from the modifier text; `isAsync` is always `false`. +- `signature` for methods and constructors is the text up to the body. +- `parameters`: `name`, `type`, `isRest` (true for varargs); `isOptional` is always `false`. +- `deprecated`: set only from a Javadoc `@deprecated` tag. The `@Deprecated` annotation does not set it. + +### Javadoc handling + +A comment is used only when it is a `/** ... */` block that is the immediately preceding named sibling of the declaration. Lines are split into description text and `@tag` lines: + +| Javadoc | Result | +|---|---| +| Plain text lines | Joined with spaces into `description` | +| `@param name text` | Tag `{ tag: "param", name, description }` | +| `@return text` | Tag `{ tag: "returns", description }` | +| `@throws Type text` / `@exception Type text` | An entry in `throws` and a tag `{ tag: "throws", name: Type, description }` | +| `@since`, `@deprecated` | Generic tag; also fills `since` / `deprecated` | +| Any other tag | Generic tag `{ tag, description }` | + +`//` and `/* */` comments are ignored. + +--- + +## 5. Spring Boot Endpoint Extraction + +For each method, the first annotation matching `Get|Post|Put|Delete|Patch|Request` + `Mapping` produces an `EndpointInfo`. + +| Annotation | `httpMethod` | +|---|---| +| `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping` | `GET`, `POST`, `PUT`, `DELETE`, `PATCH` | +| `@RequestMapping` | The `RequestMethod.X` found in the annotation text, otherwise `ANY` | + +**Path.** The method path comes from a `path = "..."` or `value = "..."` attribute, or the first string literal in the annotation. A class-level mapping annotation is used as a prefix. The two are joined, repeated slashes are collapsed and a trailing slash is removed (`/api` + `users/{id}` becomes `/api/users/{id}`). + +**Parameters.** + +| Parameter annotation | Recorded as | +|---|---| +| `@PathVariable` | `pathVariables`, `required: true`; name from `name` / `value` / the literal, else the Java parameter name | +| `@RequestParam` | `queryParameters`; `required` is false when `required = false` is given or a `defaultValue` exists; `defaultValue` is captured | +| `@RequestBody` | `requestBody` (the parameter type) | + +**Response type.** `ResponseEntity` is unwrapped to `T`; other return types are used as written. + +Example, taken from the package test (`UserController.getUser`): + +```json +{ + "httpMethod": "GET", + "path": "/api/users/{id}", + "pathVariables": [{ "name": "id", "type": { "raw": "String" }, "required": true }], + "queryParameters": [{ "name": "verbose", "type": { "raw": "boolean" }, "required": false, "defaultValue": "false" }], + "requestBody": null, + "responseType": { "raw": "UserDto" } +} +``` + +(Type objects are abbreviated; the real objects also carry `name`, `isArray`, `isNullable`, `isUnion` and `typeArguments`.) + +--- + +## 6. Error Handling & Known Limitations + +### Error behaviour + +- `initialize` throws if the WASM grammar cannot be loaded. +- `parse` reads each file with `fs.readFileSync`; an unreadable file throws and aborts the whole call. There is no per-file recovery. +- tree-sitter is error tolerant, so a file with syntax errors still yields a partial tree. Only a `null` tree is skipped. +- `validate` always reports valid. + +### Not supported *(by code reading)* + +- `record` declarations and `@interface` annotation types are not modules. +- Nested and inner types are not modules, and their members are not visited from the outer type. +- Array-valued mapping paths such as `value = {"/a", "/b"}` are not parsed into separate routes. +- `DecoratorNode.arguments` is always `{}`; only the raw annotation text is kept. +- Interface `extends` lists are not recorded as `implements` (the parser reads the grammar's `interfaces` field only). +- `isNullable` and `isUnion` on type references are always `false`; arrays are recognised only by a trailing `[]`. +- **Parameter descriptions are always empty.** The description lookup reads a Javadoc comment before the parameter list rather than before the method, so no comment is found. `@param` text is still available in the member's `tags`. No test covers this. + +--- + +## 7. Integration Guide + +- **With renderers.** In exhaustive mode `@docgen/renderer-html` shows endpoint data on a member as a method badge, a path and the response type. See [renderer-html.md](renderer-html.md). `@docgen/renderer-pdf` does not render endpoint details separately; see [renderer-pdf.md](renderer-pdf.md). +- **CLI.** Run `reposcribe-cli generate` with a `java` language entry; `--format` selects the outputs (markdown, html, pdf) and `--mode` selects developer or exhaustive. See [cli.md](cli.md). This repository's own `.docgen.yaml` keeps the Java block commented out. +- **Mixed projects.** TypeScript and Java entries can be combined in one `.docgen.yaml`; `fixtures/e2e` does this and is exercised by `pnpm test:e2e`. + +--- + +## 8. Internal Implementation Notes + +- Private helpers: `parseTypeDeclaration`, `parseMember`, `memberBase`, `parseParameters`, `extractEndpoint`, `findParameterNodes`, `extractAnnotations`, `javadoc`, `parseTypeParameters`, `typeRef`, `unwrapResponseEntity`, `splitGeneric`, `extractPackage`, `modifierText`, `visibility`, `compactSignature`, `mappingPath`, `requestMappingMethod`, `annotationName`, `annotationAttribute`, `joinRoutes`. +- Annotations keep a non-enumerable `__node` reference to their syntax node so parameter annotations can be located later. +- Generic type text is split on top-level commas with a depth counter (`splitGeneric`). +- `tree.delete()` is called after each file to release WASM memory; it is skipped if an exception occurs first. + +### Tests + +`packages/parser-java/src/index.test.ts` has one test over two fixtures (`__fixtures__/PlainUser.java`, `__fixtures__/UserController.java`). It asserts the module id, kind and description, a public getter, the controller's annotations, and the `GET /api/users/{id}` and `POST /api/users` endpoints. + +```bash +pnpm build +pnpm --dir packages/parser-java test +``` + +Build first: packages resolve `@docgen/core` through its built output. diff --git a/docs/technical/renderer-html.md b/docs/technical/renderer-html.md new file mode 100644 index 0000000..ba00bf3 --- /dev/null +++ b/docs/technical/renderer-html.md @@ -0,0 +1,177 @@ +# @docgen/renderer-html -- Technical Reference + + + +> **Evidence.** Written from `packages/renderer-html/src/index.ts` and its test at RepoScribe commit `4cbc9e4` (2026-08-11). Statements marked *(by code reading)* are not covered by a test. The test suite was not re-run when this document was written. + +## 1. Overview + +`@docgen/renderer-html` is the static HTML site renderer. It takes a transformed DocIR and returns the files of a self-contained documentation site: one HTML page per documentation page, a stylesheet, a small client script and a search index. It has no runtime dependency other than `@docgen/core`, and generating the site needs no network access. + +**Where it fits:** the render layer, next to `@docgen/renderer-markdown` and `@docgen/renderer-pdf`. + +``` +Source Code --> [Parsers] --> DocIR --> [Transformers] --> DocIR --> [@docgen/renderer-html] --> static site + ^^^^^^^^^^^^^^^^^^^^^^ +``` + +- **npm package:** `@docgen/renderer-html` v1.1.0 +- **Dependency:** `@docgen/core` (workspace) +- **Source file:** `packages/renderer-html/src/index.ts` (410 lines, single file) +- **Export:** `HtmlRenderer` class (named and default export) +- **Plugin type:** `renderer`; `format = "html"`; `supports = ["html", "static-site"]` + +--- + +## 2. Installation & Setup + +The orchestrator loads `@docgen/renderer-html` automatically when `output.html.enabled` is `true`. + +```yaml +project: + name: My Project + repository: https://github.com/example/my-project # enables source links + +output: + html: + enabled: true + outputDir: docs-site + baseUrl: /my-project/ # use "/" when served from a domain root + search: true + options: + sourceBranch: main # branch used in source links + +documentation: + mode: developer # or "exhaustive" +``` + +### Settings the renderer reads + +| Setting | Effect | Default | +|---|---|---| +| `output.html.baseUrl` | Prefix for every link and asset. Normalised to start and end with `/`. | `/` | +| `output.html.search` | Adds the search box and fetches `search-index.json` on demand | `true` | +| `output.html.options.sourceBranch` | Branch name in source links | `main` | +| `project.repository` | Base URL for source links; without it, source paths are plain text | none | +| `documentation.mode` | `developer` (planned guide pages) or `exhaustive` (one page per module) | `developer` | + +`engine`, `theme` and `sidebar` are accepted by the config schema but **are not read** by this renderer *(by code reading)*; the site always uses its built-in layout and styles. `outputDir` is used by the caller that writes the artifacts; the renderer itself returns files in memory. + +Programmatic use: + +```typescript +import { HtmlRenderer } from "@docgen/renderer-html"; + +const renderer = new HtmlRenderer(); +await renderer.initialize({ projectConfig: config, workDir: process.cwd(), options: {}, logger: console }); +const artifacts = await renderer.render(docir, config.output); // OutputArtifact[] +``` + +--- + +## 3. Architecture + +### Single-file design + +`HtmlRenderer` implements `RendererPlugin`. Page bodies, the layout, the client script and the stylesheet are built from string templates in the same file. There is no template engine and no bundler. + +### Render flow + +```mermaid +graph TD + A["render(docir)"] --> B{"documentation.mode"} + B -->|developer| C["createDeveloperPages: pages from docir.documentationPlan"] + B -->|exhaustive| D["createExhaustivePages: index, per-language index, per-module pages"] + C --> E["renderLayout per page"] + D --> E + E --> F["page .html artifacts"] + A --> G["assets/styles.css"] + A --> H["assets/app.js"] + A --> I["search-index.json"] +``` + +### Modes + +- **developer** builds pages from `docir.documentationPlan`: README, architecture, project structure, setup, one page per feature, API, components, state, testing and troubleshooting. The plan page `README.md` (or `index.md`) becomes `index.html`; other `.md` paths become `.html`. The home page gets a "Documentation map" card grid, and the architecture page gets a Mermaid flowchart (sources, DocIR, Markdown, HTML, PDF) built from up to four tech-stack entries that match TypeScript, JavaScript, Java, React or Spring. +- **exhaustive** builds `index.html`, `/index.html` and `/.html`. Module pages show badges, props for React components and a members list. Private members are omitted. + +--- + +## 4. Public API Reference + +```typescript +export class HtmlRenderer implements RendererPlugin { + readonly name = "@docgen/renderer-html"; + readonly version = "1.1.0"; + readonly type = "renderer"; + readonly format = "html"; + readonly supports = ["html", "static-site"]; + + async initialize(config: PluginConfig): Promise; + async validate(): Promise; // always valid + async render(docir: DocIR): Promise; + async cleanup(): Promise; // no-op +} +``` + +`render` takes only the DocIR; the second argument the orchestrator passes (the output config) is ignored. Each `OutputArtifact` has `filePath`, `content`, `mimeType`, `size` and `metadata` (`generatedAt`, `sourceModules`, `format: "html"`). + +--- + +## 5. Output Structure + +| Artifact | MIME type | Notes | +|---|---|---| +| One `.html` per page | `text/html` | Shared layout: skip link, top bar, sidebar navigation, optional search, content, footer | +| `assets/styles.css` | `text/css` | Embedded in the renderer; responsive; the sidebar collapses behind a Menu button below 800 px | +| `assets/app.js` | `text/javascript` | Search and menu toggle (a short inline script) | +| `search-index.json` | `application/json` | One entry per page: `title`, `path`, `summary` and `text` (tags stripped, first 4000 characters) | + +**Search.** The browser fetches `search-index.json` on the first keystroke and filters entries by case-insensitive substring over title, summary and text. It shows up to 8 results. No search engine library is used. + +**Source links.** Each page lists up to 8 of its source paths. With `project.repository` set, a path links to `/blob//`; a trailing `.git` is removed. Without it the path is shown as plain code text. + +**Escaping.** All interpolated text and attribute values pass through `escapeHtml` / `escapeAttribute`. The only inline formatting supported in section text is backtick code spans. + +--- + +## 6. Known Limitations + +- **Diagrams need the network when a page is viewed.** Every page includes a module script that imports Mermaid from `https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs`. The major version is not pinned to an exact release and no integrity hash is set. Offline, or behind a firewall that blocks the CDN, diagrams show as plain text. Generation itself is offline. +- **Developer mode requires a plan.** Without `docir.documentationPlan` the renderer throws `Developer HTML output requires a documentation plan.` (Exhaustive mode does not need one.) +- **Exhaustive mode name collisions.** Page names are `/.html`. Two modules with the same name and language write the same file *(by code reading)*. Overloaded members on one page share an element id. +- **Exhaustive mode renders modules only.** It has no ADR or changelog pages. +- **No per-site theming.** `theme`, `sidebar` and `engine` are ignored (see section 2). +- **Search is basic.** Substring match only, no ranking, and only the first 4000 characters of each page are indexed. + +--- + +## 7. Integration Guide + +- **Producing a site:** set `output.html.enabled: true` and run `reposcribe-cli generate --format html` (`-f` is the short form; `--mode developer|exhaustive` overrides the mode). The `docs:generate` script requests Markdown and PDF only, so it does not build the HTML site. See [cli.md](cli.md). +- **GitHub Pages:** set `baseUrl` to `//` so links and assets resolve under the project path, then publish `outputDir`. `docs/guides/ci-cd-integration.md` shows CI setups. +- **Java projects:** endpoint data from `@docgen/parser-java` is shown on member entries (method badge, path, response type) in exhaustive mode. See [parser-java.md](parser-java.md). +- **Other formats:** the same DocIR also feeds [renderer-markdown.md](renderer-markdown.md) and [renderer-pdf.md](renderer-pdf.md). + +--- + +## 8. Internal Implementation Notes + +- Module-level helpers: `collectPlanPages`, `toHtmlPath`, `normalizeBaseUrl`, `renderTable`, `formatInline`, `escapeHtml`, `escapeAttribute`, `stripTags`, `safeFileName`, `slug`, `displayLanguage`, `mermaidText`. +- `CLIENT_SCRIPT` and `STYLES` are string constants emitted verbatim as artifacts. +- The generator version in the footer comes from `docir.metadata.generatorVersion`. +- Mermaid is initialised with `securityLevel: 'strict'`. + +### Tests + +`packages/renderer-html/src/index.test.ts` has two tests: + +1. A developer-mode site with `baseUrl: /demo/`: checks the expected artifacts exist (`index.html`, `architecture.html`, the asset files, `search-index.json`), that the architecture page contains a Mermaid block, the prefixed stylesheet URL and the GitHub source link. +2. An exhaustive-mode run: checks that `typescript/Widget.html` is produced. + +```bash +pnpm build +pnpm --dir packages/renderer-html test +``` + +The end-to-end smoke test (`pnpm test:e2e`) also renders HTML for the `fixtures/e2e` project. diff --git a/docs/technical/renderer-pdf.md b/docs/technical/renderer-pdf.md new file mode 100644 index 0000000..b756337 --- /dev/null +++ b/docs/technical/renderer-pdf.md @@ -0,0 +1,178 @@ +# @docgen/renderer-pdf -- Technical Reference + + + +> **Evidence.** Written from `packages/renderer-pdf/src/index.ts` and its test at RepoScribe commit `4cbc9e4` (2026-08-11). Statements marked *(by code reading)* are not covered by a test. The test suite was not re-run when this document was written. + +## 1. Overview + +`@docgen/renderer-pdf` produces a single combined PDF developer guide from a transformed DocIR. It writes the PDF file format itself (PDF 1.4) and has no PDF, browser or converter dependency, so it needs no network and no headless browser. The output is text-first: cover page, linked table of contents and the guide pages, with headings, paragraphs, bullet lists, code blocks and tables. + +**Where it fits:** the render layer. + +``` +Source Code --> [Parsers] --> DocIR --> [Transformers] --> DocIR --> [@docgen/renderer-pdf] --> documentation.pdf + ^^^^^^^^^^^^^^^^^^^^^ +``` + +- **npm package:** `@docgen/renderer-pdf` v1.1.0 +- **Dependency:** `@docgen/core` (workspace) +- **Source file:** `packages/renderer-pdf/src/index.ts` (948 lines, single file) +- **Export:** `PdfRenderer` class (named and default export) +- **Plugin type:** `renderer`; `format = "pdf"`; `supports = ["pdf"]` + +--- + +## 2. Installation & Setup + +The orchestrator loads `@docgen/renderer-pdf` automatically when `output.pdf.enabled` is `true`. + +```yaml +output: + pdf: + enabled: true + outputDir: docs/pdf + options: + fileName: my-project-developer-guide.pdf + +documentation: + mode: developer +``` + +### Settings the renderer reads + +| Setting | Effect | Default | +|---|---|---| +| `output.pdf.options.fileName` | Name of the single output file (trimmed; must be a non-empty string) | `documentation.pdf` | +| `documentation.mode` | `developer` uses the documentation plan; `exhaustive` produces a module summary | `developer` | + +`engine` (`puppeteer` or `pandoc`) and `branding` (logo, primary colour, company name) are accepted by the config schema but **are not read** by this renderer *(by code reading)*. Neither Puppeteer nor Pandoc is used or required, even though the schema default for `engine` is `puppeteer`. `outputDir` is used by the caller that writes the artifact. + +Programmatic use: + +```typescript +import { PdfRenderer } from "@docgen/renderer-pdf"; + +const renderer = new PdfRenderer(); +await renderer.initialize({ projectConfig: config, workDir: process.cwd(), options: {}, logger: console }); +const [artifact] = await renderer.render(docir, config.output); // artifact.content is a Buffer +``` + +--- + +## 3. Architecture + +### Two phases + +1. **Layout.** Documentation pages are laid out onto page drafts (`PdfPageDraft`): a list of PDF drawing commands plus link annotations. Page size is A4 (595 x 842 points) with 54 pt side margins. +2. **Serialisation.** `buildPdf` writes the objects, page streams, link annotations, cross-reference table and trailer by hand. + +### Fonts + +Three standard PDF fonts are referenced by name and not embedded: Helvetica (body), Helvetica-Bold (headings) and Courier (code). + +### Render flow + +```mermaid +graph TD + A["render(docir)"] --> B{"developer mode and documentationPlan?"} + B -->|yes| C["buildDeveloperPages"] + B -->|no| D["buildExhaustivePages (module summary)"] + C --> E["layoutDocumentationBody"] + E --> F["estimate TOC pages, shift anchors"] + F --> G["cover + TOC pages + body pages"] + D --> H["Reference pages"] + G --> I["buildPdf"] + H --> I + I --> J["documentation.pdf artifact"] +``` + +If the mode is `developer` but the DocIR has no documentation plan, the renderer silently produces the exhaustive summary layout. (The HTML renderer throws in the same situation.) + +--- + +## 4. Public API Reference + +```typescript +export class PdfRenderer implements RendererPlugin { + readonly name = "@docgen/renderer-pdf"; + readonly version = "1.1.0"; + readonly type = "renderer"; + readonly format = "pdf"; + readonly supports = ["pdf"]; + + async initialize(config: PluginConfig): Promise; + async validate(): Promise; // always valid + async render(docir: DocIR): Promise; // exactly one artifact + async cleanup(): Promise; // no-op +} +``` + +`render` takes only the DocIR; the second argument the orchestrator passes is ignored. The returned artifact has `mimeType: "application/pdf"`, a `Buffer` as `content`, and `metadata` with `generatedAt`, `sourceModules` and `format: "pdf"`. + +--- + +## 5. Document Layout + +### Developer mode + +| Part | Content | +|---|---| +| Cover | Title (` Developer Guide`), description, generation date, mode, page count, module count, guide count, Node version (or "project-specific"), up to four setup steps as a quick start | +| Table of contents | One entry per guide and per section, with page numbers and clickable links; spills onto more pages when long | +| Body | Plan pages in order: README, architecture, project structure, setup, each feature, API, components, state, testing, troubleshooting | + +Every body page has a header (project and page title) and a footer with the page number and total. Within a page: + +- A "Relevant Paths" card lists the page's source paths (first 6, then `+ N more paths`). +- **Paragraphs and bullets** are word-wrapped. +- **Code blocks** are drawn in Courier on a dark panel, in chunks of 18 lines. Lines are not wrapped or clipped. +- **Tables** use equal-width columns, wrap cell text, and alternate row shading. + +### Exhaustive mode (module summary) + +A "Reference" section lists, for each module: name and kind, path, coverage percentage and the first sentence of its description. It does not list members, endpoints or props. + +--- + +## 6. Known Limitations + +- **ASCII only.** Text is normalised before drawing: en and em dashes become `-`, curly quotes become straight quotes, the bullet becomes `*`, and **every other character outside printable ASCII is replaced with a space**. Accented letters, non-Latin scripts and emoji are lost. +- **No images, charts or diagrams.** Diagram source (for example Mermaid) placed in a code block is printed as code text, not drawn. See `docs/guides/pdf-images-and-charts.md`. +- **No inline formatting.** Backticks and other Markdown markers in plan text are printed as-is. +- **Approximate text metrics.** Wrapping assumes 0.53 x font size per character. A word longer than a line is split once at the line limit, so a very long unbroken string can still run past the margin. +- **Long code lines overflow** the panel's right edge instead of wrapping. +- **Minimal PDF features.** PDF 1.4, uncompressed, no bookmarks/outline, no document title metadata. Only the TOC link annotations are interactive. +- **Locale-dependent date.** The cover date uses `toLocaleDateString("en-IN")`. +- **In-memory build.** The whole file is assembled as one string before it is converted to a `Buffer`. +- **Shared anchor registry.** The TOC link targets are stored on `globalThis.__reposcribePdfAnchors` between layout and serialisation. Both steps run synchronously inside one `render` call, so renders do not interleave, but the registry keeps its last value afterwards. + +--- + +## 7. Integration Guide + +- **Producing a PDF:** set `output.pdf.enabled: true` and run `reposcribe-cli generate --format pdf`, or run the repository's `docs:generate` script, which requests Markdown and PDF. See [cli.md](cli.md). +- **Combining outputs:** the same DocIR feeds [renderer-markdown.md](renderer-markdown.md) and [renderer-html.md](renderer-html.md). +- **CI:** archive the PDF as a build artifact; see `docs/guides/ci-cd-integration.md`. +- **Java projects:** in exhaustive mode Java modules are listed in the module summary. Endpoint details from `@docgen/parser-java` are not rendered separately by this renderer. + +--- + +## 8. Internal Implementation Notes + +- Layout helpers: `layoutDocumentationBody`, `renderSection`, `renderParagraph`, `renderBulletList`, `renderCodeBlock`, `renderTable`, `renderRelevantPathsCard`, `buildCoverPage`, `buildTocPages`, `ensureSpace`. +- Drawing helpers: `drawText`, `drawRect`, `drawLine`, `drawWrappedText` (plus `drawTextCommands` / `drawLineCommands` for page chrome). +- Text helpers: `wrapText`, `normalizePdfText`, `estimateTextWidth`, `escapePdfText`. +- `estimateTocPageCount` reserves TOC pages before layout so that page numbers in the TOC are final. +- Anchor keys are `page:` and `section:#`. + +### Tests + +`packages/renderer-pdf/src/index.test.ts` has one test. It renders a ten-page documentation plan and checks that the output starts with `%PDF`, has the `application/pdf` type, and contains the "Table of Contents" text, a `/Subtype /Link` annotation and several page titles. + +```bash +pnpm build +pnpm --dir packages/renderer-pdf test +``` + +The test imports `../dist/index.js`, so the package must be built first.