From f8738761d3a54f767821a484f31c0184ef42f7c5 Mon Sep 17 00:00:00 2001 From: Ma'sum Date: Sat, 29 Aug 2026 19:50:39 +0700 Subject: [PATCH] feat: enhance schema with customizable theme properties and advanced header/footer styling configurations --- .vscode/settings.json | 3 + .../cli/markforge/docs/full-all-properties.md | 89 +++ apps/cli/markforge/markforge.full-config.ts | 118 ++++ apps/cli/markforge/package.json | 3 +- .../src/content/docs/markforge/changelog.mdx | 12 + .../content/docs/markforge/configuration.mdx | 356 +++++----- .../docs/src/content/docs/markforge/index.mdx | 90 ++- .../content/docs/markforge/installation.mdx | 23 +- .../docs/src/content/docs/markforge/usage.mdx | 273 ++++---- packages/markforge/README.md | 505 ++++++-------- packages/markforge/document.docx | Bin 0 -> 11883 bytes packages/markforge/document.pdf | Bin 0 -> 157937 bytes packages/markforge/schema.json | 234 ++++++- .../markforge/src/config/resolveConfig.ts | 387 +++++++++++ packages/markforge/src/config/types.ts | 285 ++++++-- .../markforge/src/core/docx/docxBuilder.ts | 637 +++++++++++++----- .../markforge/src/core/html/htmlBuilder.ts | 128 ++-- .../markforge/src/core/html/htmlThemes.ts | 101 ++- packages/markforge/src/core/pdf/pdfBuilder.ts | 143 ++-- .../src/core/syntax/syntaxHighlighter.ts | 83 ++- packages/markforge/src/index.ts | 29 +- packages/markforge/src/ui/Header.tsx | 10 +- packages/markforge/src/version.ts | 4 +- packages/markforge/test/allProperties.test.ts | 259 +++++++ packages/markforge/test/config.test.ts | 63 ++ packages/markforge/tsconfig.build.json | 5 +- packages/markforge/tsconfig.json | 5 +- 27 files changed, 2741 insertions(+), 1104 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 apps/cli/markforge/docs/full-all-properties.md create mode 100644 apps/cli/markforge/markforge.full-config.ts create mode 100644 packages/markforge/document.docx create mode 100644 packages/markforge/document.pdf create mode 100644 packages/markforge/src/config/resolveConfig.ts create mode 100644 packages/markforge/test/allProperties.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..3662b37 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/apps/cli/markforge/docs/full-all-properties.md b/apps/cli/markforge/docs/full-all-properties.md new file mode 100644 index 0000000..9337e90 --- /dev/null +++ b/apps/cli/markforge/docs/full-all-properties.md @@ -0,0 +1,89 @@ +--- +title: "Unified Platform Architecture & Document Suite" +subtitle: "Enterprise Specification & Feature Validation Guide" +author: "Masum RPG" +company: "Masum Dev Technologies" +version: "1.0.0" +date: "2026-08-29" +lang: "en" +--- + +# Executive Summary + +This sample document validates **100% of all properties** supported by **MarkForge Document Engine**: + +- **Metadata & Dynamic Tokens**: Title, Subtitle, Author, Date, Version, Company in headers and footers. +- **Table of Contents (TOC)**: Automatically extracted from Markdown heading hierarchy. +- **Watermark Engine**: Custom angle, opacity, font size, and color. +- **Syntax Highlighting**: Theme-aware tokenizer applied to multi-language code snippets. +- **Visual Callouts**: GitHub-style alert callouts rendered with border and background styling. +- **Tables & Typography**: Formatted tables with text alignment. + +--- + +# Architecture Pipeline + +MarkForge transforms Markdown directly into professional Word documents, print-ready PDFs, and responsive web pages. + +```mermaid +graph LR + MD["Markdown (.md)"] --> AST["MarkForge AST"] + AST --> DOCX["DOCX Document"] + AST --> PDF["Chromium PDF Engine"] + AST --> HTML["Self-Contained HTML"] +``` + +--- + +# Multi-Language Code Blocks + +The code blocks below are styled according to the configured `syntaxTheme`: + +```typescript +import { compileMarkdown } from "@masumdev/markforge"; + +export async function generateReleaseNotes(): Promise { + const result = await compileMarkdown({ + content: "# Release v1.0.0\n\nAll features verified.", + config: { + to: ["docx", "pdf", "html"], + theme: "corporate", + syntaxTheme: "dracula", + }, + }); + console.log(`Generated ${result.files.length} output files.`); +} +``` + +```python +import os + +def check_environment(): + api_key = os.getenv("API_KEY") + if not api_key: + raise ValueError("Missing API key configuration") + return {"status": "ready", "env": "production"} +``` + +--- + +# Callout Verifications + +> [!NOTE] +> All running header tokens (`{title}`, `{author}`, `{version}`) are evaluated dynamically at compile time. + +> [!TIP] +> Use `--watch` flag during local authoring for instant live updates. + +> [!IMPORTANT] +> Both Word Twips and CSS `@page` dimensions are accurately synchronized. + +--- + +# Benchmark Metrics + +| Pipeline Component | Target Output | Processing Time | Quality Level | +| :--- | :--- | :---: | :---: | +| Native DOCX Packager | `sample.docx` | 45ms | Pixel-Perfect Word Layout | +| Headless Chromium | `sample.pdf` | 650ms | 300 DPI Vector PDF | +| HTML5 Bundler | `sample.html` | 18ms | Single Self-Contained File | diff --git a/apps/cli/markforge/markforge.full-config.ts b/apps/cli/markforge/markforge.full-config.ts new file mode 100644 index 0000000..87a1530 --- /dev/null +++ b/apps/cli/markforge/markforge.full-config.ts @@ -0,0 +1,118 @@ +import { + defineConfig, + OutputFormat, + Theme, + Orientation, + PaperSizeEnum, + SyntaxTheme, + WatermarkPosition, +} from "@masumdev/markforge"; + +/** + * MarkForge Complete Specification Configuration Sample + * Demonstrates 100% of all configurable properties in MarkforgeConfig with full Enum autocompletion. + */ +export default defineConfig({ + // Target output formats (using OutputFormat enum or strings) + to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], + + // Output directory + outputDir: ".temp/output-full-spec", + + // Visual document theme (using Theme.CORPORATE preset or a custom ThemeProps object) + theme: Theme.CORPORATE, + + // Page orientation (using Orientation enum or strings) + orientation: Orientation.PORTRAIT, + + // Standard paper size (using PaperSizeEnum or strings) + paperSize: PaperSizeEnum.A4, + + // Page margins with CSS unit or pt numbers + margins: { + top: "3cm", + bottom: "2.5cm", + left: "2.5cm", + right: "2.5cm", + }, + + // Document running headers with dynamic variable substitution & per-zone slot styling + header: { + // Left slot: Can be a plain string template OR a rich HeaderFooterSlot object + left: { + text: "{company}", + color: "#0D998D", + fontSize: 9, + fontFamily: "Inter, Segoe UI, sans-serif", + bold: true, + }, + // Center slot: Plain string template using default header styles + center: "Internal", + // Right slot: Custom colored and italicized version tag + right: { + text: "v{version}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + // Horizontal separator line under header + divider: false, + }, + + // Document running footers with dynamic page numbering & per-zone slot styling + footer: { + // Left slot: Author metadata token + left: { + text: "Author: {author}", + color: "#64748B", + fontSize: 8.5, + }, + // Center slot: Document generation date token + center: { + text: "{date}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + // Right slot: Dynamic page numbering token with custom brand color + right: { + text: "Page {page} of {pages}", + color: "#0D998D", + fontSize: 9, + bold: true, + }, + // Horizontal separator line above footer + divider: false, + }, + + // Automatic Table of Contents generation + toc: true, + + // Watermark configuration (using WatermarkPosition enum or strings) + watermark: { + text: "CONFIDENTIAL DRAFT", + color: "#E11D48", + opacity: 0.1, + fontSize: 52, + rotate: -45, + position: WatermarkPosition.DIAGONAL, + }, + + // Code block syntax highlighting theme (using SyntaxTheme enum or strings) + syntaxTheme: SyntaxTheme.DRACULA, + + // Assets and bundling flags + embedImages: true, + bundleHtml: true, + + // Fallback metadata dictionary + metadata: { + title: "Complete MarkForge Reference Manual", + subtitle: "Enterprise Document Compiler & Visual Architecture", + author: "Masum RPG", + company: "Masum Dev Technologies", + version: "1.0.0", + date: "2026-08-29", + lang: "en", + }, +}); diff --git a/apps/cli/markforge/package.json b/apps/cli/markforge/package.json index c04793b..73c2dd7 100644 --- a/apps/cli/markforge/package.json +++ b/apps/cli/markforge/package.json @@ -4,11 +4,12 @@ "private": true, "type": "module", "scripts": { - "test:all": "bun run test:ts && bun run test:json && bun run test:yaml && bun run test:rc && bun run test:auto", + "test:all": "bun run test:ts && bun run test:json && bun run test:yaml && bun run test:rc && bun run test:full-spec && bun run test:auto", "test:ts": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md -c ./markforge.config.ts", "test:json": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md -c ./markforge.config.json", "test:yaml": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md -c ./markforge.config.yaml", "test:rc": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md -c ./.markforgerc", + "test:full-spec": "bun ../../../packages/markforge/dist/cli.mjs ./docs/full-all-properties.md -c ./markforge.full-config.ts", "test:auto": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md", "build:sample": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md --to docx,pdf,html -o .temp/output", "build:spec": "bun ../../../packages/markforge/dist/cli.mjs ./docs/full-specification.md --to docx,pdf,html -o .temp/output", diff --git a/apps/docs/src/content/docs/markforge/changelog.mdx b/apps/docs/src/content/docs/markforge/changelog.mdx index 2b2e8da..2f0aed6 100644 --- a/apps/docs/src/content/docs/markforge/changelog.mdx +++ b/apps/docs/src/content/docs/markforge/changelog.mdx @@ -6,6 +6,18 @@ sidebar: description: Release history and version notes for @masumdev/markforge. --- +## [0.2.4] - 2026-08-29 + +### πŸš€ Features & Enhancements + +- **Granular Header & Footer Slot Customization (`HeaderFooterSlot`)**: Each running header and footer zone (`left`, `center`, `right`) can now be individually styled with custom `color`, `fontSize` (in points), `fontFamily`, `bold`, and `italic`. Fully implemented across DOCX TextRuns and PDF CSS Paged Media (`@top-*` and `@bottom-*`). +- **Flagship `Theme.CORPORATE` Preset & `ThemeProps` Interface**: Simplified built-in themes to `Theme.CORPORATE` while providing the typed `ThemeProps` interface for total custom document theming (`backgroundColor`, `textColor`, `primaryColor`, `fontFamily`, `fontMono`, `customCss`). +- **TypeScript Enums**: Full autocompletion and type-safety with `OutputFormat`, `Theme`, `Orientation`, `PaperSizeEnum`, `SyntaxTheme`, and `WatermarkPosition`. +- **Background-Layered Watermark**: Watermarks are positioned strictly at the background layer (`z-index: 0`) under content to avoid obscuring tables, text, or images. +- **Enterprise Windows Sandboxing**: Headless Chromium runner is sandboxed with `--disable-sync`, isolated temp directory, and zero Google network requests, prioritizing `msedge.exe` on Windows. + +--- + ## [0.2.3] - 2026-08-28 ### πŸ› οΈ Fixed diff --git a/apps/docs/src/content/docs/markforge/configuration.mdx b/apps/docs/src/content/docs/markforge/configuration.mdx index 91eee61..aef9bd0 100644 --- a/apps/docs/src/content/docs/markforge/configuration.mdx +++ b/apps/docs/src/content/docs/markforge/configuration.mdx @@ -1,193 +1,209 @@ --- -title: Configuration Reference -sidebar: - label: Configuration - order: 4 -description: Complete options reference for markforge.config.ts, .markforgerc.json, and YAML configurations with header, footer, watermark, and theme options. +title: "Configuration" +description: "Complete guide to MarkForge configuration files, TypeScript enums, custom ThemeProps, and document styling." --- -import { Tabs, TabItem, Aside } from '@astrojs/starlight/components'; +import { Tabs, TabItem, Aside } from "@astrojs/starlight/components"; -MarkForge supports configuration via file or frontmatter. Configuration files are auto-discovered in the following order: - -1. `markforge.config.ts` / `markforge.config.js` / `markforge.config.mjs` -2. `markforge.config.json` / `.markforgerc.json` / `.markforgerc` -3. `markforge.config.yaml` / `markforge.config.yml` - ---- - -## Configuration File Examples - - - - ```typescript - // markforge.config.ts - import { defineConfig } from "@masumdev/markforge"; - - export default defineConfig({ - to: ["docx", "pdf", "html"], - outputDir: "./dist", - theme: "default", - orientation: "portrait", - paperSize: "A4", - margins: { - top: "2.5cm", - bottom: "2.5cm", - left: "3cm", - right: "3cm", - }, - header: { - left: "My Organization", - center: "{title}", - right: "v{version}", - }, - footer: { - left: "Confidential β€” Internal Use Only", - right: "Page {page} of {pages}", - }, - toc: true, - embedImages: true, - watermark: { - enabled: false, - text: "DRAFT", - opacity: 0.07, - position: "diagonal", - }, - }); - ``` - - - ```json - { - "$schema": "https://raw.githubusercontent.com/masumdev/react-native-library/main/packages/markforge/schema.json", - "to": ["docx", "pdf", "html"], - "outputDir": "./dist", - "theme": "academic", - "orientation": "portrait", - "paperSize": "A4", - "toc": true, - "header": { - "left": "My Company", - "right": "{title}" - }, - "footer": { - "right": "Page {page} of {pages}" - } - } - ``` - - - ```yaml - to: - - docx - - pdf - - html - outputDir: "./dist" - theme: "academic" - orientation: "portrait" - paperSize: "A4" - toc: true - header: - left: "My Company" - right: "{title}" - footer: - right: "Page {page} of {pages}" - ``` - - +MarkForge provides a type-safe, declarative configuration system. You can configure documents using `markforge.config.ts`, `markforge.config.json`, `markforge.config.yaml`, or directly via Markdown frontmatter. --- -## All Configuration Properties +## Configuration Hierarchy -### Core Options +MarkForge automatically discovers and resolves configuration using a strict 3-tier precedence hierarchy: -| Property | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `to` | `string \| string[]` | `["docx","pdf"]` | Output formats: `docx`, `pdf`, `html` | -| `outputDir` | `string` | `undefined` | Target directory for generated files | -| `theme` | `string` | `"default"` | Built-in theme name | -| `css` | `string \| string[]` | `undefined` | Custom CSS file paths to inject | -| `embedImages` | `boolean` | `true` | Inline all images as Base64 | -| `toc` | `boolean` | `false` | Auto-generate Table of Contents | -| `watch` | `boolean` | `false` | Watch input file and recompile on change | +``` +Markdown Frontmatter (Highest Priority) + └── Project Config File (markforge.config.ts / .json / .yaml) + └── Engine Default Settings (DEFAULT_CONFIG) +``` -### Page Layout +--- -| Property | Type | Default | Description | -| :--- | :--- | :--- | :--- | -| `orientation` | `"portrait" \| "landscape"` | `"portrait"` | Page orientation | -| `paperSize` | `"A4" \| "letter" \| "legal"` | `"A4"` | Paper size | -| `margins.top` | `string` | `"2.5cm"` | Top margin (CSS units) | -| `margins.bottom` | `string` | `"2.5cm"` | Bottom margin | -| `margins.left` | `string` | `"3cm"` | Left margin | -| `margins.right` | `string` | `"3cm"` | Right margin | +## TypeScript Config (`markforge.config.ts`) + +Using `defineConfig` with TypeScript Enums provides **100% autocompletion and type-safety**: + +```typescript +import { + defineConfig, + OutputFormat, + Theme, + Orientation, + PaperSizeEnum, + SyntaxTheme, + WatermarkPosition, +} from "@masumdev/markforge"; + +export default defineConfig({ + // Target output formats + to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], + + // Output directory + outputDir: ".temp/output-full-spec", + + // Visual document theme (Theme.CORPORATE or custom ThemeProps object) + theme: Theme.CORPORATE, + + // Page orientation & standard size + orientation: Orientation.PORTRAIT, + paperSize: PaperSizeEnum.A4, + + // Page margins + margins: { + top: "3cm", + bottom: "2.5cm", + left: "2.5cm", + right: "2.5cm", + }, + + // Running headers with per-zone styling & tokens + header: { + left: { + text: "{company} - {title}", + color: "#0D998D", + fontSize: 9, + bold: true, + }, + center: "Internal Technical Guide", + right: { + text: "v{version}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + divider: true, + dividerColor: "#CBD5E1", + }, + + // Running footers with dynamic page numbering + footer: { + left: { + text: "Author: {author}", + color: "#64748B", + fontSize: 8.5, + }, + center: { + text: "{date}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + right: { + text: "Page {page} of {pages}", + color: "#0D998D", + fontSize: 9, + bold: true, + }, + divider: true, + dividerColor: "#CBD5E1", + }, + + // Automatic Table of Contents + toc: true, + + // Watermark configuration + watermark: { + text: "CONFIDENTIAL DRAFT", + color: "#E11D48", + opacity: 0.1, + fontSize: 52, + rotate: -45, + position: WatermarkPosition.DIAGONAL, + }, + + // Syntax highlighting palette + syntaxTheme: SyntaxTheme.DRACULA, + + // Assets & bundling flags + embedImages: true, + bundleHtml: true, + + // Global document metadata + metadata: { + title: "Unified Platform Architecture & Document Suite", + subtitle: "Enterprise Specification & Feature Validation Guide", + author: "Masum RPG", + company: "Masum Dev Technologies", + version: "1.0.0", + date: "2026-08-29", + lang: "en", + }, +}); +``` -### Header & Footer +--- -Multi-zone header and footer with Left / Center / Right columns. Supported in **DOCX** and **PDF**. +## Per-Zone Header & Footer Customization + +Each zone (`left`, `center`, `right`) can be defined either as a simple string or as a typed `HeaderFooterSlot` object with granular font, color, size, bold, and italic controls: + +```typescript +header: { + left: { + text: "{company} - {title}", + color: "#0D998D", + fontSize: 9, + fontFamily: "Inter, Segoe UI, sans-serif", + bold: true, + }, + right: { + text: "v{version}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + divider: true, + dividerColor: "#CBD5E1", +} +``` -| Property | Type | Description | -| :--- | :--- | :--- | -| `header.left` | `string` | Left-aligned header content | -| `header.center` | `string` | Center-aligned header content | -| `header.right` | `string` | Right-aligned header content | -| `footer.left` | `string` | Left-aligned footer content | -| `footer.center` | `string` | Center-aligned footer content | -| `footer.right` | `string` | Right-aligned footer content | - -**Available tokens:** - -| Token | Replaced With | -| :--- | :--- | -| `{title}` | Document title from frontmatter | -| `{author}` | Author from frontmatter | -| `{version}` | Version from frontmatter | -| `{date}` | Date from frontmatter | -| `{page}` | Current page number | -| `{pages}` | Total page count | - -### Watermark - - - -| Property | Type | Default | Description | +| Slot Property | Type | Default | Description | | :--- | :--- | :--- | :--- | -| `watermark.enabled` | `boolean` | `false` | Enable/disable watermark | -| `watermark.text` | `string` | `"DRAFT"` | Watermark text | -| `watermark.opacity` | `number` | `0.07` | Opacity 0–1 | -| `watermark.position` | `string` | `"diagonal"` | `diagonal` \| `center` \| `top-left` \| `top-right` \| `bottom-left` \| `bottom-right` | +| `text` | `string` | **Required** | Template text (supports `{title}`, `{company}`, `{author}`, `{version}`, `{date}`, `{page}`, `{pages}`) | +| `color` | `string` | `"#94A3B8"` | Custom hex or named color string | +| `fontSize` | `number` | `9` | Font size in points (pt) | +| `fontFamily` | `string` | `"Segoe UI"` | Font family for this specific slot | +| `bold` | `boolean` | `false` | Whether text is bold weight | +| `italic` | `boolean` | `false` | Whether text is in italics | --- -## Built-in Themes - -| Theme | Font | Best Use Case | -| :--- | :--- | :--- | -| `default` | System sans-serif (Segoe UI, Roboto) | Tech specs, API docs | -| `academic` | Merriweather / Georgia (serif) | Papers, reports, theses | -| `github` | GitHub Markdown style | READMEs, open-source | -| `corporate` | Clean professional layout | Business documents | -| `minimal` | Whitespace-focused | Simple notes | -| `dracula` | Dark-mode inspired | Developer docs | - -All themes share the `THEME_COMPONENTS` base layer β€” callout boxes, code blocks, tables, TOC, and images are consistently styled across every theme. +## Custom Document Theming (`ThemeProps`) + +In addition to `Theme.CORPORATE`, you can pass a full `ThemeProps` object to completely brand your documents: + +```typescript +export default defineConfig({ + theme: { + primaryColor: "#0D998D", + primaryDark: "#008073", + primaryLight: "#D9F1F0", + backgroundColor: "#FFFFFF", + textColor: "#0F172A", + textMuted: "#64748B", + borderColor: "#E2E8F0", + cardBackground: "#F8FAFC", + fontFamily: "'Inter', sans-serif", + fontMono: "'Fira Code', monospace", + }, +}); +``` --- -## Frontmatter vs Config File +## Dynamic Metadata Tokens -Frontmatter properties (in the Markdown file itself) always **override** config file settings for that specific document: +Variables defined in `metadata` or Markdown frontmatter can be referenced inside headers, footers, and templates using `{token}` syntax: -```markdown ---- -title: "Quarterly Report" -theme: "corporate" # overrides config file theme -toc: false # overrides config file toc: true -watermark: - enabled: true - text: "CONFIDENTIAL" ---- -``` +| Token | Source | Example Output | +| :--- | :--- | :--- | +| `{title}` | Document title | `Platform Reference Guide` | +| `{subtitle}` | Document subtitle | `Enterprise Architecture` | +| `{author}` | Author name | `Masum RPG` | +| `{version}` | Document version | `1.0.0` | +| `{date}` | Document date | `2026-08-29` | +| `{company}` | Company or organization | `Masum Dev Technologies` | +| `{page}` | Current page number (Footers only) | `1` | +| `{pages}` | Total pages count (Footers only) | `4` | diff --git a/apps/docs/src/content/docs/markforge/index.mdx b/apps/docs/src/content/docs/markforge/index.mdx index b76b770..8c714ab 100644 --- a/apps/docs/src/content/docs/markforge/index.mdx +++ b/apps/docs/src/content/docs/markforge/index.mdx @@ -6,28 +6,28 @@ sidebar: description: Modern, high-performance Markdown & MDX multi-format publishing engine. Convert Markdown to DOCX, PDF, and HTML with Mermaid diagrams, callout boxes, syntax highlighting, and a beautiful Ink terminal UI. --- -import { Card, CardGrid, Badge } from '@astrojs/starlight/components'; +import { Card, CardGrid } from '@astrojs/starlight/components'; -**MarkForge** is a high-performance Markdown & MDX document publishing engine and CLI. It converts a single Markdown source into pixel-perfect **Microsoft Word (DOCX)**, **PDF**, and **standalone HTML** β€” with Mermaid diagrams, GitHub-style callout boxes, dual-theme syntax highlighting, image inlining, and a beautiful Ink terminal UI. +**MarkForge** is an enterprise-grade Markdown & MDX document publishing engine and CLI. It compiles a single Markdown source into pixel-perfect **Microsoft Word (DOCX)**, **PDF**, and **standalone HTML** β€” with native Mermaid diagrams, GitHub-style callout alert boxes, dual-theme syntax highlighting, customizable `ThemeProps`, per-zone header/footer slots, and an interactive Ink terminal UI. Generate `.docx`, `.pdf`, and `.html` simultaneously from a single Markdown source. - - Flowcharts, sequence diagrams, gantt charts, and class diagrams β€” rendered and embedded in all formats. + + Flagship cyan corporate design system + full custom colors and fonts via `ThemeProps`. - - GitHub-style `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`, `> [!IMPORTANT]` with full color styling. + + Granular color, font size, font family, bold, and italic controls per `left`, `center`, and `right` slot. - - Dark theme for PDF/HTML, Light theme for DOCX β€” automatically selected per format. + + Flowcharts, sequence diagrams, gantt charts, and class diagrams β€” rendered and embedded natively. - - Resolves local paths, remote URLs (auto-fetched), Base64 data URIs, and SVG vectors. + + GitHub-style `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]` with rich colored styling. - - Left / Center / Right multi-zone headers and footers with `{title}`, `{page}`, `{pages}` tokens. + + Dark theme for PDF/HTML, Light theme for DOCX β€” optimized per document format. @@ -51,43 +51,66 @@ import { Card, CardGrid, Badge } from '@astrojs/starlight/components'; | Horizontal rules | βœ… | βœ… | βœ… | | Bold, italic, strikethrough | βœ… | βœ… | βœ… | | Auto Table of Contents | βœ… | βœ… | βœ… | -| **Header & Footer (multi-zone)** | βœ… | βœ… | β€” | +| **Header & Footer (per-zone slots)** | βœ… | βœ… | β€” | | Embedded ` - -
-
91ms
Compilation Time
-
100%
Type Safety
-
3 Formats
DOCX | PDF | HTML
-
-``` - --- ## Programmatic API - + ```typescript - import { markforge } from "@masumdev/markforge"; + import { + compileMarkdown, + OutputFormat, + Theme, + Orientation, + PaperSizeEnum, + WatermarkPosition, + } from "@masumdev/markforge"; - const result = await markforge("./specification.md", { - to: ["docx", "pdf", "html"], + const result = await compileMarkdown("./specification.md", { + to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], outputDir: "./dist", - theme: "academic", + theme: Theme.CORPORATE, + orientation: Orientation.PORTRAIT, + paperSize: PaperSizeEnum.A4, + toc: true, + watermark: { + text: "CONFIDENTIAL", + color: "#E11D48", + opacity: 0.08, + position: WatermarkPosition.DIAGONAL, + }, metadata: { - title: "System Architecture Specification", - author: "Ma'sum", - version: "2.4.0", + title: "Platform Reference Manual", + author: "Masum RPG", + company: "Masum Dev Technologies", + version: "1.0.0", }, + }, (statusMessage) => { + console.log(`[PROGRESS] ${statusMessage}`); }); - console.log(`βœ“ ${result.files.length} files in ${result.durationMs}ms`); - for (const f of result.files) { - console.log(` [${f.format.toUpperCase()}] ${f.filePath}`); + console.log(`βœ“ Compiled ${result.files.length} documents in ${result.durationMs}ms`); + for (const file of result.files) { + console.log(` [${file.format.toUpperCase()}] ${file.filePath} (${file.sizeBytes} bytes)`); } ``` - + ```typescript + import * as fs from "node:fs"; import { - compileMarkdown, - buildPdfDocument, + parseMarkdownDocument, buildDocxDocument, + buildPdfDocument, buildHtmlDocument, + Theme, } from "@masumdev/markforge"; - // Parse once β€” build each format independently - const { doc, config, baseDir } = await compileMarkdown("./report.md"); - - const pdf = await buildPdfDocument(doc, config, baseDir); - const docx = await buildDocxDocument(doc, config, baseDir); - const html = await buildHtmlDocument(doc, config, baseDir); - - // Use buffers/strings directly - await Bun.write("./dist/report.pdf", pdf); - await Bun.write("./dist/report.docx", docx); - await Bun.write("./dist/report.html", html); + // 1. Read and parse Markdown into AST + const rawMarkdown = fs.readFileSync("./report.md", "utf-8"); + const doc = parseMarkdownDocument(rawMarkdown); + + const config = { + theme: Theme.CORPORATE, + toc: true, + margins: { top: "2.5cm", bottom: "2.5cm", left: "2.5cm", right: "2.5cm" }, + }; + + // 2. Build formats independently + const docxBuffer = await buildDocxDocument(doc, config); + const pdfBuffer = await buildPdfDocument(doc, config); + const htmlString = await buildHtmlDocument(doc, config); + + // 3. Save files + fs.writeFileSync("./dist/report.docx", docxBuffer); + fs.writeFileSync("./dist/report.pdf", pdfBuffer); + fs.writeFileSync("./dist/report.html", htmlString, "utf-8"); ``` diff --git a/packages/markforge/README.md b/packages/markforge/README.md index 08c413c..501c917 100644 --- a/packages/markforge/README.md +++ b/packages/markforge/README.md @@ -2,8 +2,8 @@ # @masumdev/markforge -**Modern, high-performance Markdown & MDX multi-format publishing engine & CLI.** -Convert Markdown into pixel-perfect **DOCX**, **PDF**, and **HTML** with syntax highlighting, Mermaid diagrams, callout boxes, and a beautiful Ink terminal UI. +**Enterprise Markdown & MDX Multi-Format Publishing Engine & CLI.** +Convert Markdown into pixel-perfect **DOCX**, **PDF**, and **HTML** with native typography, syntax highlighting, Mermaid diagrams, callout boxes, customizable `ThemeProps`, and an interactive Ink terminal UI. [![npm version](https://img.shields.io/npm/v/@masumdev/markforge.svg?style=flat-square&color=33CDCF)](https://www.npmjs.com/package/@masumdev/markforge) [![License: MIT](https://img.shields.io/badge/License-MIT-33CDCF.svg?style=flat-square)](https://opensource.org/licenses/MIT) @@ -18,381 +18,282 @@ Convert Markdown into pixel-perfect **DOCX**, **PDF**, and **HTML** with syntax | Feature | Description | | :--- | :--- | -| πŸ“„ **Multi-Format Output** | Generate `.docx`, `.pdf`, and `.html` from a single Markdown source | -| 🎨 **Mermaid Diagrams** | Full Mermaid.js support β€” flowcharts, sequence, gantt, class diagrams | -| πŸ“’ **Callout / Alert Boxes** | GitHub-style `> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`, `> [!IMPORTANT]` | -| 🌈 **Dual Syntax Highlighting** | Dark theme for PDF/HTML, Light theme for DOCX β€” 100% readable in both | -| πŸ–ΌοΈ **Image Inlining Engine** | Auto-resolves local paths, remote URLs, Base64 URIs, and SVGs | -| πŸ“‘ **Auto Table of Contents** | Anchored TOC generated from heading structure | -| πŸ“ **Header & Footer Zones** | Left / Center / Right multi-zone headers and footers per format | -| 🎨 **Curated Themes** | `default`, `academic`, `github`, `corporate`, `minimal`, `dracula` | -| πŸ’§ **Watermark Support** | Optional configurable watermark (position, opacity, text) β€” off by default | -| βš™οΈ **Type-Safe Config** | `markforge.config.ts`, `.markforgerc.json`, or YAML with JSON Schema | -| πŸš€ **Programmatic API** | Full TypeScript API for Node.js & Bun integration | -| πŸ–₯️ **Ink Terminal UI** | Rich interactive TUI with progress indicators and build summary | +| πŸ“„ **Multi-Format Output** | Generate `.docx`, `.pdf`, and `.html` from a single Markdown source simultaneously | +| 🎨 **Theme.CORPORATE & ThemeProps** | Flagship cyan corporate design system + full custom colors & fonts via `ThemeProps` | +| πŸ“ **Per-Zone Header & Footer** | Granular color, font size, font family, bold & italic controls per `left`, `center`, `right` slot | +| πŸ“Š **Mermaid Diagrams** | Full Mermaid.js diagram engine β€” flowcharts, sequence, state, class, gantt | +| πŸ“’ **Callout / Alert Boxes** | GitHub-style `> [!NOTE]`, `> [!TIP]`, `> [!IMPORTANT]`, `> [!WARNING]`, `> [!CAUTION]` | +| 🌈 **Syntax Highlighting** | Tokenized multi-language code blocks (`dracula`, `github-dark`, `monokai`, `nord`, etc.) | +| πŸ–ΌοΈ **Image Inlining Engine** | Resolves local files, remote URLs, Base64 data URIs, and SVGs into embedded assets | +| πŸ“‘ **Auto Table of Contents** | Anchored TOC generated from heading structure (`#` to `####`) | +| πŸ’§ **Non-Intrusive Watermark** | Background-layer watermark with custom text, color, opacity, rotation, and position | +| βš™οΈ **Type-Safe Config** | `markforge.config.ts` with Enums, `.markforgerc.json`, or YAML with JSON Schema | +| πŸš€ **Programmatic API** | High-level `compileMarkdown()` and low-level AST document builders for Node.js & Bun | +| πŸ–₯️ **Ink Terminal UI** | Interactive TUI with live spinners, progress tracking, and compilation metrics | --- ## πŸ“¦ Installation ```bash -# Global CLI +# Global CLI Installation npm install -g @masumdev/markforge +# or with bun: bun add -g @masumdev/markforge -# Dev dependency +# Project Local Dependency npm install -D @masumdev/markforge +# or with bun: bun add -d @masumdev/markforge ``` --- -## πŸš€ Quick Start +## πŸ’» CLI Usage ```bash -# Convert to DOCX + PDF (default) +# Basic conversion to DOCX and PDF (defaults) markforge document.md -# Specify output formats +# Specify target formats and output folder markforge document.md --to docx,pdf,html -o ./dist -# Use a theme and custom CSS -markforge report.md --theme academic --css ./styles/corp.css +# Use custom CSS styling and force Table of Contents +markforge specification.md --css ./styles/custom.css --toc -# Force Table of Contents -markforge specification.md --toc +# Watch mode for live re-compilation +markforge report.md --watch -# Watch mode -markforge document.md --watch +# Launch local preview server and open in browser +markforge document.md --serve --port 4000 --open ``` ---- - -## πŸ–ŠοΈ Markdown Source Features - -### Callout / Alert Boxes - -MarkForge supports GitHub-style alert syntax with full color-coded styling in all output formats: - -```markdown -> [!NOTE] -> This is a note β€” rendered with cyan left border. - -> [!TIP] -> This is a tip β€” rendered with green styling. - -> [!IMPORTANT] -> Critical information β€” rendered with purple styling. - -> [!WARNING] -> A warning β€” rendered with amber/yellow styling. - -> [!CAUTION] -> High-risk action β€” rendered with red styling. -``` - -### Mermaid Diagrams - -Mermaid code blocks are automatically rendered into images embedded in all output formats: - -````markdown -```mermaid -flowchart LR - A[Markdown Source] --> B[AST Parser] - B --> C[DOCX Builder] - B --> D[PDF Builder] - B --> E[HTML Builder] -``` -```` - -### Syntax-Highlighted Code Blocks - -All common languages are supported with proper token coloring: - -````markdown -```typescript -import { markforge } from "@masumdev/markforge"; - -const result = await markforge("./spec.md", { to: ["pdf", "docx"] }); -``` -```` - -**Dual-theme coloring:** -- **PDF / HTML**: VS Code Dark+ palette (bright colors on dark background) -- **DOCX**: GitHub Light palette (deep colors on white background) +### CLI Flags Reference -### Table of Contents - -Add `toc: true` in frontmatter or use `--toc` CLI flag to auto-generate: - -```markdown ---- -toc: true ---- - -# My Document -``` - ---- - -## πŸ“‹ Frontmatter Reference - -```markdown ---- -title: "Enterprise Architecture Specification" -subtitle: "Cloud & Edge Infrastructure β€” Q3 2026" -author: "Ma'sum" -version: "2.4.0" -date: "2026-08-27" -theme: "default" # default | academic | github | corporate | minimal | dracula -toc: true # auto-generate Table of Contents -orientation: "portrait" # portrait | landscape -paperSize: "A4" # A4 | letter | legal -watermark: - enabled: false # no watermark by default - text: "CONFIDENTIAL" - opacity: 0.08 - position: "diagonal" # diagonal | center | top-left | top-right | bottom-left | bottom-right -header: - left: "My Company" - center: "{title}" - right: "Version {version}" -footer: - left: "Confidential" - right: "Page {page} of {pages}" ---- -``` +| Flag | Alias | Description | Default | +| :--- | :---: | :--- | :--- | +| `` | | Markdown input file path | **Required** | +| `--to ` | `-t` | Output formats: `docx`, `pdf`, `html`, `png` | `docx,pdf` | +| `--output ` | `-o` | Output directory | Same as input file | +| `--config ` | `-c` | Explicit configuration file path | Auto-discovered | +| `--theme ` | | Visual theme preset (`corporate`, `default`) | `corporate` | +| `--css ` | | Custom CSS stylesheets to inject | `undefined` | +| `--orientation ` | | Page orientation (`portrait`, `landscape`) | `portrait` | +| `--paper-size ` | | Standard paper size (`A4`, `Letter`, `Legal`, `A3`, `A5`) | `A4` | +| `--toc` | | Force Table of Contents generation | `false` | +| `--watermark ` | | Document watermark text | `undefined` | +| `--syntax-theme `| | Code syntax highlighting theme | `github-dark` | +| `--watch` | `-w` | Watch input file and re-compile on change | `false` | +| `--serve` | | Start local HTTP preview server | `false` | +| `--port ` | `-p` | Local preview server port | `4000` | +| `--open` | | Automatically open browser on preview | `false` | +| `--version` | `-V` | Output version number | | +| `--help` | `-h` | Display CLI help menu | | --- -## βš™οΈ Configuration Reference +## βš™οΈ Configuration (`markforge.config.ts`) -MarkForge automatically discovers configuration files starting from the input file's directory up to the workspace root, or via the `-c, --config ` CLI flag. - -### 1. TypeScript (`markforge.config.ts`) +MarkForge supports type-safe configuration with full TypeScript Enums: ```typescript -import { defineConfig } from "@masumdev/markforge"; +import { + defineConfig, + OutputFormat, + Theme, + Orientation, + PaperSizeEnum, + SyntaxTheme, + WatermarkPosition, +} from "@masumdev/markforge"; export default defineConfig({ - // Output formats: "docx" | "pdf" | "html" - to: ["docx", "pdf", "html"], - outputDir: "./dist/documents", + // Target output formats + to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], + + // Output destination directory + outputDir: ".temp/output-docs", - // Theme: "default" | "academic" | "github" | "corporate" | "minimal" | "dracula" - theme: "academic", + // Visual document theme (Theme.CORPORATE preset or a custom ThemeProps object) + theme: Theme.CORPORATE, - // Custom CSS stylesheet(s) to inject - css: ["./styles/custom.css"], + // Page orientation & standard physical paper size + orientation: Orientation.PORTRAIT, + paperSize: PaperSizeEnum.A4, - // Page layout & dimensions - orientation: "portrait", // "portrait" | "landscape" - paperSize: "A4", // "A4" | "Letter" | "Legal" | "A3" | "A5" + // Document page margins margins: { - top: "2.5cm", + top: "3cm", bottom: "2.5cm", - left: "3cm", - right: "3cm", + left: "2.5cm", + right: "2.5cm", }, - // Multi-zone header & footer (supports tokens: {title}, {author}, {version}, {date}, {page}, {pages}) + // Running headers with dynamic tokens & per-zone slot styling header: { - left: "Enterprise Architecture", - center: "{title}", - right: "v{version}", + left: { + text: "{company} - {title}", + color: "#0D998D", + fontSize: 9, + fontFamily: "Inter, Segoe UI, sans-serif", + bold: true, + }, + center: "Internal Technical Guide", + right: { + text: "v{version}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + divider: true, + dividerColor: "#CBD5E1", }, + + // Running footers with dynamic page numbering & per-zone slot styling footer: { - left: "Confidential β€” Internal Use Only", - right: "Page {page} of {pages}", + left: { + text: "Author: {author}", + color: "#64748B", + fontSize: 8.5, + }, + center: { + text: "{date}", + color: "#94A3B8", + fontSize: 8.5, + italic: true, + }, + right: { + text: "Page {page} of {pages}", + color: "#0D998D", + fontSize: 9, + bold: true, + }, + divider: true, + dividerColor: "#CBD5E1", + }, + + // Automatic Table of Contents + toc: true, + + // Watermark configuration (placed at lowest background layer) + watermark: { + text: "CONFIDENTIAL DRAFT", + color: "#E11D48", + opacity: 0.1, + fontSize: 52, + rotate: -45, + position: WatermarkPosition.DIAGONAL, }, - // Document features - toc: true, // Auto-generate Table of Contents - embedImages: true, // Embed/inline all images as Base64 data URIs - bundleHtml: true, // Self-contained HTML with embedded styles and scripts - syntaxTheme: "github-dark", // "github-dark" | "github-light" | "dracula" | "monokai" | "nord" + // Syntax highlighting theme + syntaxTheme: SyntaxTheme.DRACULA, - // Diagonal page watermark - watermark: "CONFIDENTIAL", + // Asset embedding & bundling flags + embedImages: true, + bundleHtml: true, - // Default document metadata + // Global document metadata metadata: { - title: "System Architecture Specification", - author: "Ma'sum", + title: "Unified Platform Architecture & Document Suite", + subtitle: "Enterprise Specification & Feature Validation Guide", + author: "Masum RPG", + company: "Masum Dev Technologies", version: "1.0.0", - company: "My Organization", + date: "2026-08-29", + lang: "en", }, - - // Server & watch options - watch: false, - serve: false, - port: 4000, - open: false, }); ``` -### 2. JSON with `$schema` (`markforge.config.json`) - -Adding `$schema` enables **instant autocompletion and validation** in VS Code, WebStorm, and other IDEs: - -```json -{ - "$schema": "https://raw.githubusercontent.com/masumdev/react-native-library/main/packages/markforge/schema.json", - "to": ["docx", "pdf", "html"], - "outputDir": "./dist/documents", - "theme": "academic", - "orientation": "portrait", - "paperSize": "A4", - "margins": { - "top": "2.5cm", - "bottom": "2.5cm", - "left": "3cm", - "right": "3cm" - }, - "header": { - "left": "Enterprise Architecture", - "right": "{title}" - }, - "footer": { - "left": "Confidential", - "right": "Page {page} of {pages}" - }, - "toc": true, - "embedImages": true, - "syntaxTheme": "github-dark", - "watermark": "CONFIDENTIAL" -} -``` +--- -### 3. YAML (`markforge.config.yaml` / `.markforgerc.yaml`) - -```yaml -to: - - docx - - pdf - - html -outputDir: ./dist/documents -theme: academic -orientation: portrait -paperSize: A4 -margins: - top: 2.5cm - bottom: 2.5cm - left: 3cm - right: 3cm -header: - left: Enterprise Architecture - right: "{title}" -footer: - left: Confidential - right: "Page {page} of {pages}" -toc: true -embedImages: true -watermark: CONFIDENTIAL -``` +## 🎨 Custom Theme Customization (`ThemeProps`) +You can completely rebrand all document styling by passing a `ThemeProps` object: + +```typescript +import { defineConfig } from "@masumdev/markforge"; + +export default defineConfig({ + theme: { + primaryColor: "#0D998D", + primaryDark: "#008073", + primaryLight: "#D9F1F0", + backgroundColor: "#0F172A", + textColor: "#E2E8F0", + textMuted: "#94A3B8", + borderColor: "#334155", + cardBackground: "#1E293B", + codeBackground: "#020617", + codeText: "#E2E8F0", + fontFamily: "'Inter', sans-serif", + fontMono: "'Fira Code', monospace", + }, +}); +``` --- -## πŸ’» Programmatic API +## πŸš€ Programmatic API + +### High-Level API (`compileMarkdown` / `markforge`) ```typescript -import { markforge, compileMarkdown } from "@masumdev/markforge"; +import { compileMarkdown, OutputFormat } from "@masumdev/markforge"; -// High-level: compile file β†’ write to disk -const result = await markforge("./specification.md", { - to: ["docx", "pdf", "html"], +const result = await compileMarkdown("./specification.md", { + to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], outputDir: "./dist", - theme: "academic", + toc: true, metadata: { - title: "System Architecture Specification", - author: "Ma'sum", - version: "2.4.0", + title: "API Reference Manual", + author: "Masum Dev", + version: "1.0.0", }, }); -console.log(`βœ“ Generated ${result.files.length} files in ${result.durationMs}ms`); - +console.log(`βœ“ Compiled ${result.files.length} documents in ${result.durationMs}ms:`); for (const file of result.files) { - console.log(` [${file.format.toUpperCase()}] ${file.filePath} β€” ${file.sizeBytes} bytes`); + console.log(` [${file.format.toUpperCase()}] ${file.filePath} (${file.sizeBytes} bytes)`); } ``` -```typescript -import { compileMarkdown, buildPdfDocument, buildDocxDocument, buildHtmlDocument } from "@masumdev/markforge"; - -// Low-level: parse β†’ build each format independently -const parsed = await compileMarkdown("./report.md"); - -const pdfBuffer = await buildPdfDocument(parsed.doc, parsed.config, parsed.baseDir); -const docxBuffer = await buildDocxDocument(parsed.doc, parsed.config, parsed.baseDir); -const htmlString = await buildHtmlDocument(parsed.doc, parsed.config, parsed.baseDir); -``` - ---- - -## 🎨 Built-in Themes - -| Theme | Description | Best For | -| :--- | :--- | :--- | -| `default` | Blu by BCA Digital cyan palette, modern sans-serif | Tech docs, specifications | -| `academic` | Serif typography (Merriweather/Georgia), justified text | Papers, reports, theses | -| `github` | GitHub Markdown rendering style | READMEs, open-source docs | -| `corporate` | Clean professional layout | Business documents | -| `minimal` | Minimal whitespace-focused design | Simple notes | -| `dracula` | Dark-mode inspired color scheme | Developer docs | - -All themes automatically inherit shared component styles (callouts, code blocks, tables, badges) via the `THEME_COMPONENTS` base layer. - ---- - -## πŸ–₯️ CLI Flags +### Low-Level API (AST & Individual Document Builders) -| Flag | Alias | Description | Default | -| :--- | :---: | :--- | :--- | -| `--to ` | `-t` | Comma-separated output formats: `docx`, `pdf`, `html` | `docx,pdf` | -| `--output ` | `-o` | Output directory | Same as input file dir | -| `--theme ` | | Built-in theme name | `default` | -| `--css ` | | Custom CSS file(s) to inject | | -| `--toc` | | Force Table of Contents generation | `false` | -| `--config ` | `-c` | Config file path (`markforge.config.ts`, `.json`, `.yaml`) | Auto-discovered | -| `--watch` | `-w` | Watch input and re-compile on change | `false` | -| `--version` | `-V` | Print version and exit | | -| `--help` | `-h` | Show help | | - ---- - -## πŸ—οΈ Architecture - -``` -Markdown / MDX source - β”‚ - β–Ό - β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” - β”‚ AST Parser β”‚ β†’ Frontmatter + Node tree - β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ - β”‚ - β”Œβ”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β” - β”‚ β”‚ β”‚ - β–Ό β–Ό β–Ό - DOCX PDF HTML -Builder Builder Builder - β”‚ β”‚ β”‚ - β”‚ Chromium β”‚ - β”‚ (headlessβ”‚ - β”‚ PDF) β”‚ - β–Ό β–Ό β–Ό - .docx .pdf .html +```typescript +import * as fs from "node:fs"; +import { + parseMarkdownDocument, + buildDocxDocument, + buildPdfDocument, + buildHtmlDocument, + Theme, +} from "@masumdev/markforge"; + +const markdownContent = fs.readFileSync("./report.md", "utf-8"); + +// 1. Parse Markdown into structured AST +const doc = parseMarkdownDocument(markdownContent); + +const config = { + theme: Theme.CORPORATE, + toc: true, + margins: { top: "2.5cm", bottom: "2.5cm", left: "2.5cm", right: "2.5cm" }, +}; + +// 2. Build formats independently into binary buffers or HTML strings +const docxBuffer = await buildDocxDocument(doc, config); +const pdfBuffer = await buildPdfDocument(doc, config); +const htmlString = await buildHtmlDocument(doc, config); + +// 3. Write output files +fs.writeFileSync("./dist/report.docx", docxBuffer); +fs.writeFileSync("./dist/report.pdf", pdfBuffer); +fs.writeFileSync("./dist/report.html", htmlString, "utf-8"); ``` -**Sub-systems:** -- `imageResolver` β€” Local / URL / Base64 / SVG asset resolution -- `mermaidRenderer` β€” Headless Mermaid.js diagram rasterization -- `syntaxHighlighter` β€” Dual dark/light theme tokenizer (no external deps) -- `htmlThemes` β€” `THEME_COMPONENTS` base + per-theme typography overrides - --- ## πŸ“„ License -MIT Β© [Ma'sum](https://github.com/masumrpg) +MIT Β© [Masum Dev](https://github.com/masumdev) diff --git a/packages/markforge/document.docx b/packages/markforge/document.docx new file mode 100644 index 0000000000000000000000000000000000000000..906bb67aba1e8eeae1e1db898811e389b5883a39 GIT binary patch literal 11883 zcmd6NbyQVb*Zu*dyF^&tryuE6#@Vt0}CSj^Fae^eMd_}D+eZLOADs&(30qW*>)B*Z*8iB z6s|`=ghCLa;3l>Ytj$h;n7@aCHM;xj;E^h(LIxDRfyvbKDh-#WYZd){=mAtwc7-_X zN6!%{U}sqmh8t%~KE0}Z;}@Y{w{Y2dLC!mz8bLhV4BQk!4ZIvt8(PSIENhE2;y7lv z-@}t0RH&hVYaG7Sr$>Wt%cCBZ$*plFS7QniXemO6g(k380)};?yNNHCLKzs07=9u< zdUZ{1+{Bmvtd6A{v2{vPU}nwRBt*cHg}5F32k){sfbo{4PZnW;b&Cn07yr5unTE!v zyf1(>_~Kb0nh}l9UNmL~EK{o%fLbf>LiIx}>$fzA_qW{EAG>^a#r)A_zaR;e-PHG^ z{Y$m+kWB#!IY@{(OcrYVf0mM3QaK*$tiFGBc3*=Ui%0sMB+6SuQ z{jQM~O_;*XM&zJBbGjhD%>7!`Lt803k~I*3%2p0^=FrHmU?zBRWEXyEDLDgs(*|vH z336jubVf*m*1_MOt&*S5i?kNw2*DthM?1tZ6js||kE3kic;V0(XBr!9{;Ci{Ees;F zr zR8UJYqQM0bF|z|9f~$`N%UH|S7C_7rcUA(8pQz;*O-OvMwq^KE*$L-@mQX_>l`xN| zJEQkto2C!L(Wi`_LxrDDZLnI%fKKD8O(no08zD_|bzqEB^5(_9fjy%! zpTOwNvy;K9hji|l8=GGK&$>~;-w3q(3i#Xl@50fJXPy*ly?S(Du3nUqh95X?e?jQ*?q!_AB9Rt)YWB_$8Eu)nUTaC3w#91x8 zqK^)d^fp-M#7N5hp^nZsi3B~!j;KneF5%2h^eMO>w?!VIBYAy`80L8|>jbi-vS5Z+ zG5n~`W(UG1cj5*P)5KfND6-8`=z-|TvFZ)pJ@|-8;`>A(l57LLoa$p`1}~)v;pI() zzdw|IuSB84Vqauib(YaSc>ZJ z{El$@k|DM{PdbqUs}m)*_DxMs*Lukj&($PN|X zr4bzVeQ*ep261hCMQT;&>T_?IHhb;ZG^tI7C;?wqrO`e-G%(KEAQGznyd<8Vzl0w{ z(3xfLQh&4k8LrT4gRrW`fF;a6`wwqBaM<`&hWglLsjsJxOZUacLf}3OJtRH)z;MyF zso8y9^TIU=L0QS!AeZlj(e*^P+!Nf}xjs!Ec(@zg<$?pM#b{eP;}-6XQP_E^?W}MK z1z`qPf*eWL*MxDMW%=v3_-{uk5k!~`Rh1Qg?+*&L4%qGS{+%`x>k_}Ob6)KlHA6BMKE9fPhDCh)B% zIE+Nca#g~L{W0RZ*tU!#>4)!uEZR+wEbS!Ud`hx@K>qkO2h9&&jhODyNiq9GuZBH)CTrqH1$!f?1`p!-D()>Yat z!E#dCX6_LGrb^P2%9&N{;jpb@@aJel-D))s4cn?r&T&1A(NacS9`JDW__6cGhxP%y zSq$zWK)R0yfTl@RaK@ zFyBR4byYcq&l0C}%zZ)9ym>GCv#GmAd7sbV>w4)gGKQxsc~Zq*rpnsT?7Sq7Onr3U zm@O(O00|kXp?7lQvrshaPb^)sb zW+V`A1x#+W|JWnU|J;Ei zAs;@Z-GF?QF2etFbN7qu4*MHoMRyr)%ZN9Gd+hi^t;NmVd=qiOw%bz3`^^^tfSb8z z68&sO(y`{@?um?vFFg7gj8h4EY9BqD+&$5FRz1k&69KgyD5T1@3H$uLijHP<9ljMG zYh=DSQh|+zrDObsjjj5*o9*NAZD}5fuP%9ZgQzqW(kLN~4Pmx6P=SOQdeUk6JaG74t(t)PM8sFvrdD}gRA`=ZC8(mg7pk@Zvvd1~x1||ZH4uiF4 zU(eGNddV3%9ZzWB0pdyph9b-7^s%kblxLj=$SiITJvAZM_h6A;W}syYBQz~LHLBqh zZ5wyxV|CdfJ?NNxLNeq-5+K#CgnSh`s(6ZB~K+91G63TKw@J=c0P*BMK|KwraCZcFha z4NBK%;zv)ZT-_x*rXb}+4xy?BHr^`urYQKP0mESJ-!4k7byo zo}WvlE{REI6S9-P8Dj3VJk3z{c~2>VPMiAyn1sNHTj}gYzE-eUHH<*#0PxFo>_O6< zZfh8aDaM`8oR<$}QOqbg_0`#)#G!PQAXS2!n3mjZAGRvb*iNznteHOT!r)yQQ=Ea< zXU7|s=$T6A;IG+$PS!Pf?sb4-EaA&s3`;*d3+rwtBDsJH^DT4DCjC?vNe5ITp?PGo zp0+T4wUb5i7^e_~1*H_1jBfPS)p9E*5Id$t%FS_#u{>j7uswGI^6YRS@G{A<&L?HP zSs~@P!-hdlh-9`U3m8i%#zT|V*}J9ZyUE+%ZTmFfFi#{lQeHApUV)eQjpMi#A#MS0 z%vszqL&4>0Ex+tF;!khN+R(58o7eye0KoXUxmh_{>KWRZS{Z-0bAOza6^I4R zw~X9o7ihN(>rE>FBJ~Ms_RNJ%r<$g2!xfhd92^pH+Nm+#l6V@;a4t_;ZunrYtBQ;L z>lGHa(E92(^}Y1$E;cGfEG-|}bT&oqs9r304>vq%c^#q%?ghyatdar|r`jjVVMjXVvQVVxkdX;RAFE4J`0E&KFG*G74qv;W^lUBGCM{8I0z6g5Y&TQ% zoOuwV|Kj1v{9HJKFXJBGxeF$xbJmri9u{#01@W*5A%!l#agi_@b#1`wro@!Ehas|1 zNT?95E_17mPYQ=L33$RdqvVKsaj)Tv`>A)C`cr9EL4hWLZoOpoTSz@KYl076%T*F; z;2v1guwvE6>_^EsxmMbYH9n5!&7m!RlsO;jo=vq9W-bGvgn@!EVc&irH`mpKth?JQ zg@S?vL!w1v1M~&H$XZ3R9q0Gz6!EJoXJRh)*qrkT&wBx0-y28%a zPZmX$fw!C!ORdOHdr@&9qM7%|0JWsE9}erOb|-7(otcZ@#PV^^o%7XQ|9biUbGLv2 z$LGJ*WJt0iV^_id8vw`g_u|c8&-_(_i5!%KVny@Qrs`|r0jUUQn=&h`%VhIC7dLWx z*H1#NSYmBW8>q55Efkh;wc7hc2&YJBvOsllQE`mLQUi!(X~%a$kXpOdLsT3aL^F+F z5Wsf!Ldh~fq@6T!7d?oGHhv^qx?;{D;9Z34GI3=Eb1qGC^$z2K985CXc$evXNRrod zvvIw5O8tPQtN1Gt+Rq6zZ>Z>|Y~(T*7%PNM)vf4o_8R-Q?UFvybUxQhMt{f2D#JEm zg(J73nP1QQ#g3c0%X@X=1uBtuCdHFd13O~&`VYAgv$zrqY1l;U+LF!(tZ^iNF6j}c zt6EF28D4|0hv4f+x$7^Rm>B9B7}~M^s!+)U3!p%fA&Ha;);k3<1+%vIS)u400gzQl zQG9@Uyv+<+H;tz^jc*RE_+~h;ZEcbn@Nl$KCA{8syYsxz(OP}DJLZ?cs{EJ&-4X3| znX2${1p9^(T`y_b2-`8MW{c#F%-el~9emI&?EC@L|5{5UYio!9Pqg&h&kcG84y#&l zH4*KnE$>)yC05fqi3O`W?UXV6E2fZ@3^1xts>&2KhOXoqP|Dg$*Zhey{M+c@Ocaaq z^Xwg6X1w6R-K(%c?)XbT%%GHLS{dlCkk*@!el`q{pTjiwE)Fc;!=N@?_Ju*NOy<0} zOz6-=!>4eEdmIWUpR!Ktt;gBH2aR}<-eKf{J10*V?^`I$1bC{du*Vw_iDMh(#YDwZ1d zEv9JQBVIufy&?e{BKav_)>tEL(Z`h64F1ub=@M^i>lk5Seb{)aTKEp$jre!w91I4e zf+#4sGb`397DkLqyvaUmc4yVBTG%(Ou(WIwkcQP#tu23}LnKX?kk4 z8}$3ar`f3@?lKobHbu7C>TwQFLOFILzbuKT*nMuq&TTCxC`)>v(aRo6{KdQ`o-X)C zM+Nw1{qy2M@j+|)6Ri;0)jZlcNDEZ-Oi%adwv!dt1xrAqM%FqjTR(<%MWg-U<8qj` zE&-QEyA}REh3HA0OIKUJwtgWol^N?*HU}mlaIz&^v1Yh>iir|V@w8|i^;FqvXwI}ppN1b-xb@pxr4fQ|hxL`b z5yEx-@Q}SW!^^S968Ior^?-a|&}qO?TSlQn^`^j3$#fmB5|HG4XfO7Q1~JJ85+XCg zQC}XK;TbVSYSmeTIe#Kb?`H7(>5~o)2WcuGntSS z_u9E-vYs+y*t$EG7r|( z>P>y=`Rp1t(Gyfes~gpUTgZDs0#wB;1qTTim^ovD_ajXqYbytE&p}(s#m3NH>-)xD zts8m63a|89w33a?9of&%&QgUD z_BL00YC4GpIo7|dC|5He+8|Wh_Sat>+ceZMS#uWo8KYLrE8tG7gnshCCNCsX!>2!* z$17k}lOXgJbuPI11k#Y?lLp@B)1qk951u?65|(d9ef=vu}qXw3?+R)^+7L?FJhCVg@KsI6R)}-5(N96 zuJh^*@uhp=xqPtH4DpZYLVjY;40RnH`#MO8sVlPGw(`vXXhx9NQm18(%HzcY9|#5TF#5wf!by`3FnKcUy(lc9z}uK zrn`)aA0;W5*FO3vn1uVMd(~V(_hRBMXF zBsZ0mjSW>Lwowg~h9s=_s)3-wE0GZS2<+bZu<@ z)ZcsCvJf$1r0SvVPG&u7PZcr;I~k_b>(>-sur}vs3rp44I5-u8dUE+I6@@u~s%2hT z4L)Dc0y(tty8#C8^c56_%hD#A!0`95IMC0(*bQ)VN>-$h;gw2uDTqq8F0+khn-Dhn z&`+rX`Zkp&jG;AQ9OhB>VLz8m7N2X6MOV0-I0*#x8{|EKMZ(Iqr4IA&q|A5f5fiZG za;~#q3}rfPdB`Y@N3KAFWd>L>H(ldsmTGUm0GP^A4V#JN;uxsL9na5Ip!nfChhEt^ zjF>6CNsP}icE}c!CSDUid$KAJU^LSA_M>uQ)}qmbHdHYmD(!?hd_YQu9qn*C;I^OjHlkJw{ouV?A z##z7ipW%C#JQIP#_YQgfBL;qC7ym{2%r6-l(h82!NN|+?YM&Zf8T<;;{%DZZ5-FOm zcIP5rYiRxW=fKe*VwI+%#97#MyzcxK(w39e9Va)qfg0s#&L>T*c?s4x39QA|H)b(a zZ7%S+q*H8E0W5@)rmvvS4=;81qh>6=Xfa8flES5>H~p_8bD_4csxJyY@9Ss_!aF}~ zu6{_S*-*39mk%4B*Bc5ni&M1|Pi-7SKtY=(_t12ZBW!9N$PaN4^3HJEipBBFhu}~n zp%-8xJoenLuCgc4&RNiP_pHi`G^r-9fSwvRd{$>)+^!3gO7W2}rHUXXjoN4fpJ)Sp zc!?1HwwsrBSUQn=FHX{4X=b~74Nz1IXlN+r{z%C2Dz~TCBy9EZ``|MybF%|mvl}*6 zP@;O{+r+gn9;nbJS|x8rsq#&UTg|8}T$+u8@>-L5AHz^+n+LT9>fs`slT!Q1&-n_OLaY>kF1x)FZP34brLPx3G8}C>o$Dt#aDzO2Q#l}%cUBrrubyEiY8^}$Qgu~?%Bq1g_J(rpshe95tkzCCALZVH7ahRrF~R_ zBZ%U0EWP?Wx0tukF>2^cr;nXIE}Le&d>q9w#%LwXZE|sVpYc^;s|W%&w%edaRL+#3 zR+CFftK#L~ZcEx&L@V>u9-InRgKVYASo4oL8Qx!^==la`(F{F2w=jV6XthMbh6kp$ zl7|%>TNHY06~fg^dASvePiITqs=r8}#;yRj_e=Cmlc+_?qFT_iIbv=WW%1u8-3ZIcOrG@H~itv^x#Q7E7kCZ2Y`qXM|3uzWIHb z^)#(?$uSY1b|GQQqy4h$pw-ALW7a|HD+;1vIq87C>jbiZJ?n+N`D`=Ml~qjCZ~zuM zQ19t}3u@Pi*>m3Qp@9X=T2Y(Na|;H`**^FXv0QCNk3`n7UsZl0^JRD#(Q5f`Xw=kIPo#JHKjY2x;HG@k37RXApX}|&0Wv`x3?PootIyT zz@aGwZq41{X;uzOx_TBrIs_@hvsP`SSg?DlWHa>D2_dG%a|}|iK0X6LRtux#EzFPA zl9be#FNVxwqaEC?xLKU4+H7|}c)w2HtYMUlUk23lC_X!#=+b3dl#$#IOo>%c3!Ww-@0? zr;Q%rOSSSU(Kvo^nET5IOOgJm1fY?0t(U`0cn%MN_?cs@bq%m17ZK`B&jfUopSL|_ zQe+PcbfxQ5Rri28g)Lr#IL8Fi+NT&>5c`Vv6zCuG=HU?)(G1)MCIGUttc)I=v#1Um z8b*g?8_^}Yh?IzZgP9F}9+M^(CP4fma^#D;2H}?OtF#wR<(F)eLy))){}VMuKnBe*(ct z&YwsB?n}CV+U>Wb7;p&xP~onO`|$fM=-+UDuw{P0e>bFmga27V|K4!>YkZ%6DDs{B z>^JPM$=`3l{R9F4)pub(+i~~d_Zv&U;e_BO`VW==XfyrB|5Ix)$NhVJ*SZn>BKp|| z_|sqa8v#F20Koh&%#R5E{bPU4^)BoGUNZkZcEMZAod~&8`u#r&iUH^IcLd!j?ER~t z`-SJ<8u4KLE$EIe_vdrJmiQZ;i}N@7jspKc-zzfxM)%_VYsu-pfcs^P-vSB=?nIpa zucF3%0r!gtzXe!=-E+UBa36g?ga3_Y0#oN7PWU62yN|yc4c~LX--ChpA5Q$0aQ&+r z@26?MMUDLwb(g;XPnvvZ{l7;C=^vWh74ol&+$ZM0C7F}`E$J>zzpu!B9{3y11KvY_ znDj0;xDUT;(Ld+GO!db+?h5(WdE9^N|5oJ7Pf14LlISlE@IPttr*A1}{?OzPDSxX1 U{bM5{01yBI!QXnp007|s0rUi*<^TWy literal 0 HcmV?d00001 diff --git a/packages/markforge/document.pdf b/packages/markforge/document.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6c277b7fd00b58477bde52fdb9b4bb8fde7967fb GIT binary patch literal 157937 zcmeEv2V4|OvoBe)fJhL870G#FNlMN+M?qnMB}tH+vw#RnmLMPqO3pb7l0?ZtP(Y$2 z2_gt0B5zpWxE}pH=UmTw@80)&x;i`4y*1U<^{=k(u9~4!m6T=!vqP}y2H!1BV}n7Q zAUk6#Y(YT|O-mH5J*3!%pW(rcZhB}$qIoN>2984@MonR(T&JHjpYz_$r7}Uwm z0mP_e=V57W4dvir=L8ur>Vmq?3s2sVd?7|6-N8TNBpTnNMfGVEZU|4@(j+f6myZ@@qtx+=z2Ko0@* zosFH~SEKl%<{P-wwe3ZM2 z%^~IHB%|SUC{5s}GTfj;X{ZD3=8(3tbvnEz&BX-*KEPGr8W%SR_#A(SfVdz>wGZoy zi`%(@^f`eGT)Yr42+YUF1(4SO#9-p23Uz?lIyoN7N5;X<`NkpLqZYwWfodQQO$Vs0 zlmG6cz?KIsv+BgYA~= z9oa9BDuydHxxn%P)r_Ijl*?az(PeB#0^Ru_%~&kkxZr?ebxkv`Eenfb`c2J_5NEDO z|Fats7CoV5O*>mA-N9@96MkHzno+t&WjMOn45Bn(QQ@A`ET3?8#`^NnDyh?JJUzVy zS48+uB`79+{8AZ+IcPc7Eio2i)`=@1c0h?qBUvpVd@p@6sFPJC-4{zvt*nMqa`Ax? z*E{-PMyNFYhN$BeGv^yHs zm3EWrQSKwDiIMQ+zG0!kw|q%-SsOG0^cNSF`=8t0p^O?zvARKVMO}hp#&kG2gppio z=&c8}rj1+m%kX>MgX9nPA~Sfema(>dwObz;V7?2v#dsxpX1Da66!lt*vCwJ5hDWi{ zhbTA};4vP!_<%kiNT>r9ifqB4)$^{FR!+t7CjJ|U~ypFOo zy@Pup3>SlxaHvtN)sLW!Y@I`osXF)JvK(u&LO+vqx|FQ-D+;ewb4LQEoLCg6J~iF5 z;2v6IQ=Ie)EQ0#ooDN?D`sjw{EgE;X@!oeIsB~fuwY5Vu8G!Y^4nX

qxMjs>VR

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 literal 0 HcmV?d00001 diff --git a/packages/markforge/schema.json b/packages/markforge/schema.json index b1e60fd..db23343 100644 --- a/packages/markforge/schema.json +++ b/packages/markforge/schema.json @@ -27,20 +27,36 @@ }, "outputDir": { "type": "string", - "description": "Output directory for compiled documents" + "description": "Output directory where compiled documents will be saved" }, "theme": { - "type": "string", - "description": "Visual document theme name", - "enum": ["default", "academic", "github", "corporate", "minimal", "dracula"], - "default": "default" - }, - "css": { - "description": "Custom CSS stylesheet path(s) to inject", + "description": "Visual document theme (preset 'corporate' or custom ThemeProps object)", "oneOf": [ - { "type": "string" }, - { "type": "array", "items": { "type": "string" } } - ] + { + "type": "string", + "enum": ["corporate", "default"] + }, + { + "type": "object", + "properties": { + "primaryColor": { "type": "string" }, + "primaryDark": { "type": "string" }, + "primaryLight": { "type": "string" }, + "backgroundColor": { "type": "string" }, + "textColor": { "type": "string" }, + "textMuted": { "type": "string" }, + "borderColor": { "type": "string" }, + "cardBackground": { "type": "string" }, + "codeBackground": { "type": "string" }, + "codeText": { "type": "string" }, + "fontFamily": { "type": "string" }, + "fontMono": { "type": "string" }, + "customCss": { "type": "string" } + }, + "additionalProperties": false + } + ], + "default": "corporate" }, "orientation": { "type": "string", @@ -50,13 +66,13 @@ }, "paperSize": { "type": "string", - "description": "Physical page size", + "description": "Standard physical paper size", "enum": ["A4", "Letter", "Legal", "A3", "A5"], "default": "A4" }, "margins": { "type": "object", - "description": "Document margins (supports units like '2.5cm', '1in', '20mm', or pt numbers)", + "description": "Document page margins (supports units like '2.5cm', '1in', '20mm', or pt numbers)", "properties": { "top": { "type": ["string", "number"] }, "bottom": { "type": ["string", "number"] }, @@ -67,21 +83,133 @@ }, "header": { "type": "object", - "description": "Header text across pages", + "description": "Running header template across document pages (supports {title}, {subtitle}, {author}, {version}, {date}, {company})", "properties": { - "left": { "type": "string" }, - "center": { "type": "string" }, - "right": { "type": "string" } + "left": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "fontSize": { "type": "number" }, + "fontFamily": { "type": "string" }, + "bold": { "type": "boolean" }, + "italic": { "type": "boolean" } + }, + "required": ["text"], + "additionalProperties": false + } + ] + }, + "center": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "fontSize": { "type": "number" }, + "fontFamily": { "type": "string" }, + "bold": { "type": "boolean" }, + "italic": { "type": "boolean" } + }, + "required": ["text"], + "additionalProperties": false + } + ] + }, + "right": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "fontSize": { "type": "number" }, + "fontFamily": { "type": "string" }, + "bold": { "type": "boolean" }, + "italic": { "type": "boolean" } + }, + "required": ["text"], + "additionalProperties": false + } + ] + }, + "divider": { "type": "boolean" }, + "dividerColor": { "type": "string" }, + "font": { "type": "string" }, + "size": { "type": "number" }, + "color": { "type": "string" } }, "additionalProperties": false }, "footer": { "type": "object", - "description": "Footer text across pages (supports {page} and {pages} variables)", + "description": "Running footer template across document pages (supports {page}, {pages}, {title}, {author}, {date})", "properties": { - "left": { "type": "string" }, - "center": { "type": "string" }, - "right": { "type": "string" } + "left": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "fontSize": { "type": "number" }, + "fontFamily": { "type": "string" }, + "bold": { "type": "boolean" }, + "italic": { "type": "boolean" } + }, + "required": ["text"], + "additionalProperties": false + } + ] + }, + "center": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "fontSize": { "type": "number" }, + "fontFamily": { "type": "string" }, + "bold": { "type": "boolean" }, + "italic": { "type": "boolean" } + }, + "required": ["text"], + "additionalProperties": false + } + ] + }, + "right": { + "oneOf": [ + { "type": "string" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "fontSize": { "type": "number" }, + "fontFamily": { "type": "string" }, + "bold": { "type": "boolean" }, + "italic": { "type": "boolean" } + }, + "required": ["text"], + "additionalProperties": false + } + ] + }, + "divider": { "type": "boolean" }, + "dividerColor": { "type": "string" }, + "font": { "type": "string" }, + "size": { "type": "number" }, + "color": { "type": "string" } }, "additionalProperties": false }, @@ -91,37 +219,71 @@ "default": false }, "watermark": { - "type": "string", - "description": "Diagonal watermark text across document pages" - }, - "embedImages": { - "type": "boolean", - "description": "Whether to inline/embed all images as Base64 in outputs", - "default": true + "description": "Watermark configuration across document pages (string or WatermarkOptions object)", + "oneOf": [ + { "type": "string" }, + { "type": "boolean" }, + { + "type": "object", + "properties": { + "text": { "type": "string" }, + "color": { "type": "string" }, + "opacity": { "type": "number" }, + "fontSize": { "type": "number" }, + "rotate": { "type": "number" }, + "position": { + "type": "string", + "enum": ["diagonal", "center", "top-right", "bottom-right"] + } + }, + "required": ["text"], + "additionalProperties": false + } + ] }, - "bundleHtml": { - "type": "boolean", - "description": "Produce self-contained HTML with embedded styles and scripts", - "default": true + "css": { + "description": "Custom CSS stylesheet path(s) to inject into the document", + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] }, "syntaxTheme": { "type": "string", - "description": "Code block syntax highlighting color scheme", + "description": "Code block syntax highlighting color theme", "enum": ["github-dark", "github-light", "dracula", "monokai", "nord"], "default": "github-dark" }, "metadata": { "type": "object", - "description": "Document frontmatter metadata defaults", + "description": "Default document metadata dictionary (used as fallback if not in markdown frontmatter)", "properties": { "title": { "type": "string" }, - "author": { "type": "string" }, + "subtitle": { "type": "string" }, + "author": { + "oneOf": [ + { "type": "string" }, + { "type": "array", "items": { "type": "string" } } + ] + }, "date": { "type": "string" }, "version": { "type": "string" }, - "company": { "type": "string" } + "company": { "type": "string" }, + "lang": { "type": "string" }, + "coverPage": { "type": "boolean" } }, "additionalProperties": true }, + "embedImages": { + "type": "boolean", + "description": "Whether to inline/embed all images as Base64 in outputs", + "default": true + }, + "bundleHtml": { + "type": "boolean", + "description": "Produce self-contained HTML with embedded styles and scripts", + "default": true + }, "watch": { "type": "boolean", "description": "Watch mode for re-compilation on source file changes", diff --git a/packages/markforge/src/config/resolveConfig.ts b/packages/markforge/src/config/resolveConfig.ts new file mode 100644 index 0000000..f23a91b --- /dev/null +++ b/packages/markforge/src/config/resolveConfig.ts @@ -0,0 +1,387 @@ +import type { + MarkforgeConfig, + MarkforgeTheme, + DocumentOrientation, + PaperSize, + PageMargins, + HeaderFooterItem, + HeaderFooterSlot, + WatermarkOptions, +} from "./types.js"; +import { DEFAULT_CONFIG } from "./loadConfig.js"; + +/** + * Standard paper dimensions in Word Twips (1/20th of a point, 1440 twips = 1 inch). + */ +export const PAPER_DIMENSIONS_TWIP: Record = { + A4: { width: 11906, height: 16838 }, // 210mm x 297mm + Letter: { width: 12240, height: 15840 }, // 8.5in x 11in + Legal: { width: 12240, height: 20160 }, // 8.5in x 14in + A3: { width: 16838, height: 23811 }, // 297mm x 420mm + A5: { width: 8390, height: 11906 }, // 148mm x 210mm +}; + +/** + * Converts CSS margin string (e.g. "2.5cm", "1in", "20mm", "20pt", 1440) to Word Twips. + */ +export function parseMarginToTwip(margin?: string | number, defaultTwip: number = 1440): number { + if (typeof margin === "number") return margin; + if (!margin) return defaultTwip; + + const str = margin.trim().toLowerCase(); + if (str.endsWith("cm")) { + const cm = parseFloat(str); + return isNaN(cm) ? defaultTwip : Math.round(cm * 566.929); + } + if (str.endsWith("mm")) { + const mm = parseFloat(str); + return isNaN(mm) ? defaultTwip : Math.round(mm * 56.6929); + } + if (str.endsWith("in") || str.endsWith("inch")) { + const inch = parseFloat(str); + return isNaN(inch) ? defaultTwip : Math.round(inch * 1440); + } + if (str.endsWith("pt")) { + const pt = parseFloat(str); + return isNaN(pt) ? defaultTwip : Math.round(pt * 20); + } + const val = parseFloat(str); + return isNaN(val) ? defaultTwip : Math.round(val); +} + +/** + * Normalizes a margin value into a valid CSS unit string. + */ +export function formatMarginCss(margin?: string | number, defaultCss: string = "2.5cm"): string { + if (margin === undefined || margin === null) return defaultCss; + if (typeof margin === "number") return `${margin}pt`; + const str = margin.trim(); + if (!str) return defaultCss; + // If it's a raw number without unit, default to pt + if (/^[0-9.]+$/.test(str)) return `${str}pt`; + return str; +} + +export interface NormalizedMargins { + top: string; + bottom: string; + left: string; + right: string; + topTwip: number; + bottomTwip: number; + leftTwip: number; + rightTwip: number; +} + +export interface NormalizedWatermark { + text: string; + color: string; + opacity: number; + fontSize: number; + rotate: number; + position: "diagonal" | "center" | "top-right" | "bottom-right"; +} + +export interface NormalizedHeaderFooterZone { + text: string; + color: string; + fontSize: number; + fontFamily: string; + bold: boolean; + italic: boolean; +} + +export interface NormalizedHeaderFooter { + left?: NormalizedHeaderFooterZone; + center?: NormalizedHeaderFooterZone; + right?: NormalizedHeaderFooterZone; + font: string; + size: number; + color: string; + divider: boolean; + dividerColor: string; +} + +export interface ResolvedDocumentConfig { + title: string; + subtitle?: string; + author?: string; + date?: string; + version?: string; + company?: string; + lang: string; + theme: MarkforgeTheme; + orientation: DocumentOrientation; + paperSize: PaperSize; + paperDimensions: { + widthTwip: number; + heightTwip: number; + }; + margins: NormalizedMargins; + header?: NormalizedHeaderFooter; + footer?: NormalizedHeaderFooter; + toc: boolean; + watermark?: NormalizedWatermark; + css: string[]; + embedImages: boolean; + bundleHtml: boolean; + syntaxTheme: string; +} + +/** + * Replaces dynamic variables ({title}, {subtitle}, {author}, {version}, {date}, {company}) in text templates. + */ +export function replaceDocumentTokens( + template: string = "", + meta: { + title?: string; + subtitle?: string; + author?: string; + version?: string; + date?: string; + company?: string; + } +): string { + return template + .replace(/\{title\}/gi, meta.title || "") + .replace(/\{subtitle\}/gi, meta.subtitle || "") + .replace(/\{author\}/gi, meta.author || "") + .replace(/\{version\}/gi, meta.version || "") + .replace(/\{date\}/gi, meta.date || "") + .replace(/\{company\}/gi, meta.company || ""); +} + +/** + * Normalizes watermark configuration into a consistent structured object. + */ +export function normalizeWatermark( + rawWatermark?: string | WatermarkOptions | false +): NormalizedWatermark | undefined { + if (!rawWatermark) { + return undefined; + } + + if (typeof rawWatermark === "string") { + const text = rawWatermark.trim(); + if (!text) return undefined; + return { + text, + color: "#94a3b8", + opacity: 0.08, + fontSize: 54, + rotate: -45, + position: "diagonal", + }; + } + + if (typeof rawWatermark === "object") { + if (!rawWatermark.text || !rawWatermark.text.trim()) return undefined; + return { + text: rawWatermark.text.trim(), + color: rawWatermark.color || "#94a3b8", + opacity: typeof rawWatermark.opacity === "number" ? rawWatermark.opacity : 0.08, + fontSize: rawWatermark.fontSize || 54, + rotate: typeof rawWatermark.rotate === "number" ? rawWatermark.rotate : -45, + position: rawWatermark.position || "diagonal", + }; + } + + return undefined; +} + +/** + * Normalizes an individual left, center, or right header/footer zone slot. + */ +export function normalizeHeaderFooterSlot( + rawSlot: string | HeaderFooterSlot | undefined, + parent: HeaderFooterItem | undefined, + meta: { + title?: string; + subtitle?: string; + author?: string; + version?: string; + date?: string; + company?: string; + } +): NormalizedHeaderFooterZone | undefined { + if (!rawSlot) return undefined; + + if (typeof rawSlot === "string") { + const text = replaceDocumentTokens(rawSlot, meta).trim(); + if (!text) return undefined; + return { + text, + color: parent?.color || "#94A3B8", + fontSize: parent?.size || 9, + fontFamily: parent?.font || "Segoe UI", + bold: false, + italic: false, + }; + } + + if (typeof rawSlot === "object") { + const text = replaceDocumentTokens(rawSlot.text || "", meta).trim(); + if (!text) return undefined; + return { + text, + color: rawSlot.color || parent?.color || "#94A3B8", + fontSize: rawSlot.fontSize || parent?.size || 9, + fontFamily: rawSlot.fontFamily || parent?.font || "Segoe UI", + bold: Boolean(rawSlot.bold), + italic: Boolean(rawSlot.italic), + }; + } + + return undefined; +} + +/** + * Normalizes a header or footer item into a structured NormalizedHeaderFooter object. + */ +export function normalizeHeaderFooter( + raw: HeaderFooterItem | undefined, + meta: { + title?: string; + subtitle?: string; + author?: string; + version?: string; + date?: string; + company?: string; + } +): NormalizedHeaderFooter | undefined { + if (!raw) return undefined; + + const left = normalizeHeaderFooterSlot(raw.left, raw, meta); + const center = normalizeHeaderFooterSlot(raw.center, raw, meta); + const right = normalizeHeaderFooterSlot(raw.right, raw, meta); + + if (!left && !center && !right) return undefined; + + return { + left, + center, + right, + font: raw.font || "Segoe UI", + size: raw.size || 9, + color: raw.color || "#94A3B8", + divider: Boolean(raw.divider), + dividerColor: raw.dividerColor || "#E2E8F0", + }; +} + +/** + * Centralized Single Source of Truth for resolving document configuration. + * Priority hierarchy: Frontmatter metadata > Project Config File / User Config > DEFAULT_CONFIG + */ +export function resolveDocumentConfig( + frontmatter: Record = {}, + userConfig: MarkforgeConfig = {} +): ResolvedDocumentConfig { + // 1. Merge metadata defaults from config with document frontmatter + const configMeta = userConfig.metadata || {}; + const mergedMeta = { ...configMeta, ...frontmatter }; + + const title = (mergedMeta.title as string) || "MarkForge Document"; + const subtitle = (mergedMeta.subtitle as string) || undefined; + const author = Array.isArray(mergedMeta.author) + ? (mergedMeta.author as string[]).join(", ") + : (mergedMeta.author as string) || undefined; + const date = (mergedMeta.date as string) || undefined; + const version = (mergedMeta.version as string) || undefined; + const company = (mergedMeta.company as string) || undefined; + const lang = (mergedMeta.lang as string) || "en"; + + const tokenContext = { title, subtitle, author, version, date, company }; + + // 2. Visual Theme & Layout (Frontmatter > Config > Default) + const theme = (mergedMeta.theme as MarkforgeTheme) || userConfig.theme || DEFAULT_CONFIG.theme; + const orientation = (mergedMeta.orientation as DocumentOrientation) || userConfig.orientation || DEFAULT_CONFIG.orientation; + const paperSize = ((mergedMeta.paperSize as PaperSize) || userConfig.paperSize || DEFAULT_CONFIG.paperSize) as PaperSize; + + // Paper Dimensions + const baseDim = PAPER_DIMENSIONS_TWIP[paperSize] || PAPER_DIMENSIONS_TWIP.A4; + const paperDimensions = orientation === "landscape" + ? { widthTwip: baseDim.height, heightTwip: baseDim.width } + : { widthTwip: baseDim.width, heightTwip: baseDim.height }; + + // 3. Margins (Frontmatter.margins > userConfig.margins > DEFAULT_CONFIG.margins) + const fmMargin = (mergedMeta.margins as PageMargins) || {}; + const cfgMargin = userConfig.margins || {}; + const defMargin = DEFAULT_CONFIG.margins || {}; + + const topRaw = fmMargin.top ?? cfgMargin.top ?? defMargin.top ?? "2.5cm"; + const bottomRaw = fmMargin.bottom ?? cfgMargin.bottom ?? defMargin.bottom ?? "2.5cm"; + const leftRaw = fmMargin.left ?? cfgMargin.left ?? defMargin.left ?? "2.5cm"; + const rightRaw = fmMargin.right ?? cfgMargin.right ?? defMargin.right ?? "2.5cm"; + + const margins: NormalizedMargins = { + top: formatMarginCss(topRaw), + bottom: formatMarginCss(bottomRaw), + left: formatMarginCss(leftRaw), + right: formatMarginCss(rightRaw), + topTwip: parseMarginToTwip(topRaw), + bottomTwip: parseMarginToTwip(bottomRaw), + leftTwip: parseMarginToTwip(leftRaw), + rightTwip: parseMarginToTwip(rightRaw), + }; + + // 4. Header & Footer (with token substitution) + const rawHeader = (mergedMeta.header as HeaderFooterItem) || userConfig.header || DEFAULT_CONFIG.header; + const rawFooter = (mergedMeta.footer as HeaderFooterItem) || userConfig.footer || DEFAULT_CONFIG.footer; + + const header = normalizeHeaderFooter(rawHeader, tokenContext); + const footer = normalizeHeaderFooter(rawFooter, tokenContext); + + // 5. Table of Contents & Watermark + const toc = typeof mergedMeta.toc === "boolean" + ? mergedMeta.toc + : typeof userConfig.toc === "boolean" + ? userConfig.toc + : DEFAULT_CONFIG.toc; + + const rawWatermark = mergedMeta.watermark !== undefined + ? (mergedMeta.watermark as string | WatermarkOptions | false) + : userConfig.watermark !== undefined + ? userConfig.watermark + : DEFAULT_CONFIG.watermark; + + const watermark = normalizeWatermark(rawWatermark); + + // 6. Custom CSS (Combine frontmatter.css + userConfig.css) + const cssList: string[] = []; + const addCss = (item?: string | string[]) => { + if (!item) return; + if (Array.isArray(item)) cssList.push(...item); + else cssList.push(item); + }; + addCss(userConfig.css); + addCss(mergedMeta.css as string | string[] | undefined); + + // 7. Engine Flags + const embedImages = typeof userConfig.embedImages === "boolean" ? userConfig.embedImages : DEFAULT_CONFIG.embedImages; + const bundleHtml = typeof userConfig.bundleHtml === "boolean" ? userConfig.bundleHtml : DEFAULT_CONFIG.bundleHtml; + const syntaxTheme = userConfig.syntaxTheme || DEFAULT_CONFIG.syntaxTheme || "github-dark"; + + return { + title, + subtitle, + author, + date, + version, + company, + lang, + theme, + orientation, + paperSize, + paperDimensions, + margins, + header, + footer, + toc, + watermark, + css: cssList, + embedImages, + bundleHtml, + syntaxTheme, + }; +} diff --git a/packages/markforge/src/config/types.ts b/packages/markforge/src/config/types.ts index 1aea1c2..b6fee7f 100644 --- a/packages/markforge/src/config/types.ts +++ b/packages/markforge/src/config/types.ts @@ -1,17 +1,128 @@ -export type MarkforgeFormat = "docx" | "pdf" | "html" | "png"; +export enum OutputFormat { + DOCX = "docx", + PDF = "pdf", + HTML = "html", + PNG = "png", +} + +export type MarkforgeFormat = "docx" | "pdf" | "html" | "png" | OutputFormat; + +export interface ThemeProps { + /** + * Primary brand accent color (e.g. "#33CDCF", "#2563EB", "#7C3AED", "#E11D48"). + * @default "#33CDCF" + */ + primaryColor?: string; + + /** + * Dark primary tone for headings, active links, and prominent badges. + * @default "#009DA0" + */ + primaryDark?: string; + + /** + * Light primary background tint for callouts and active pills. + * @default "#ECFDFD" + */ + primaryLight?: string; + + /** + * Document page background color. + * @default "#FFFFFF" + */ + backgroundColor?: string; + + /** + * Primary body copy text color. + * @default "#0F172A" + */ + textColor?: string; + + /** + * Muted secondary text color for subtitles, dates, and footers. + * @default "#64748B" + */ + textMuted?: string; + + /** + * Border color for tables, dividers, and card containers. + * @default "#E2E8F0" + */ + borderColor?: string; + + /** + * Background color for cards, table headers, and blockquotes. + * @default "#F8FAFC" + */ + cardBackground?: string; + + /** + * Background color for code blocks. + * @default "#0F172A" + */ + codeBackground?: string; + + /** + * Text color for code blocks. + * @default "#F8FAFC" + */ + codeText?: string; + + /** + * Primary body and heading font family. + * @default "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" + */ + fontFamily?: string; + + /** + * Monospace font family for code blocks and inline code. + * @default "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace" + */ + fontMono?: string; + + /** + * Optional custom raw CSS styles appended directly to the stylesheet. + */ + customCss?: string; +} + +export enum Theme { + CORPORATE = "corporate", +} + +export type MarkforgeTheme = Theme | ThemeProps | "corporate" | (string & {}); -export type MarkforgeTheme = - | "default" - | "academic" - | "github" - | "corporate" - | "minimal" - | "dracula" - | (string & {}); +export enum Orientation { + PORTRAIT = "portrait", + LANDSCAPE = "landscape", +} + +export type DocumentOrientation = "portrait" | "landscape" | Orientation; -export type DocumentOrientation = "portrait" | "landscape"; +export enum PaperSizeEnum { + A4 = "A4", + LETTER = "Letter", + LEGAL = "Legal", + A3 = "A3", + A5 = "A5", +} -export type PaperSize = "A4" | "Letter" | "Legal" | "A3" | "A5"; +export type PaperSize = "A4" | "Letter" | "Legal" | "A3" | "A5" | PaperSizeEnum; + +export enum SyntaxTheme { + GITHUB_DARK = "github-dark", + GITHUB_LIGHT = "github-light", + DRACULA = "dracula", + MONOKAI = "monokai", + NORD = "nord", +} + +export enum WatermarkPosition { + DIAGONAL = "diagonal", + CENTER = "center", + TOP_RIGHT = "top-right", + BOTTOM_RIGHT = "bottom-right", +} export interface PageMargins { top?: string | number; @@ -20,14 +131,47 @@ export interface PageMargins { right?: string | number; } +export interface HeaderFooterSlot { + /** + * Header or footer template text (supports tokens like {title}, {author}, {version}, {date}, {company}, {page}, {pages}). + */ + text: string; + + /** + * Custom text color for this zone (e.g. "#0D998D", "#94A3B8"). + */ + color?: string; + + /** + * Custom font size in points (e.g. 9, 10). + */ + fontSize?: number; + + /** + * Custom font family for this zone (e.g. "Segoe UI", "Inter"). + */ + fontFamily?: string; + + /** + * Whether to render this zone in bold weight. + */ + bold?: boolean; + + /** + * Whether to render this zone in italics. + */ + italic?: boolean; +} + export interface HeaderFooterItem { - left?: string; - center?: string; - right?: string; + left?: string | HeaderFooterSlot; + center?: string | HeaderFooterSlot; + right?: string | HeaderFooterSlot; font?: string; size?: number; color?: string; divider?: boolean; + dividerColor?: string; } export interface WatermarkOptions { @@ -36,53 +180,34 @@ export interface WatermarkOptions { opacity?: number; fontSize?: number; rotate?: number; - position?: "diagonal" | "center" | "top-right" | "bottom-right"; + position?: "diagonal" | "center" | "top-right" | "bottom-right" | WatermarkPosition; } -export interface FrontmatterMetadata { +/** + * Pure document metadata dictionary (author, title, date, version, etc.). + */ +export interface DocumentMetadata { title?: string; subtitle?: string; author?: string | string[]; date?: string; version?: string; - theme?: MarkforgeTheme; - orientation?: DocumentOrientation; - paperSize?: PaperSize; - margins?: PageMargins; - header?: HeaderFooterItem; - footer?: HeaderFooterItem; - toc?: boolean; - watermark?: string | WatermarkOptions | false; - css?: string | string[]; - coverPage?: boolean; + company?: string; lang?: string; + coverPage?: boolean; [key: string]: unknown; } -export interface MarkforgeConfig { - /** - * Target format(s) to compile markdown to. - * @default ["docx", "pdf"] - */ - to?: MarkforgeFormat | MarkforgeFormat[]; - - /** - * Output directory where compiled documents will be saved. - * Defaults to the same directory as the input file. - */ - outputDir?: string; - +/** + * Document visual & layout options (theme, orientation, margins, headers, footers, etc.). + */ +export interface DocumentLayoutConfig { /** * Built-in visual theme name. * @default "default" */ theme?: MarkforgeTheme; - /** - * Path(s) to custom CSS stylesheets to inject into the document. - */ - css?: string | string[]; - /** * Page orientation. * @default "portrait" @@ -90,7 +215,7 @@ export interface MarkforgeConfig { orientation?: DocumentOrientation; /** - * Paper size. + * Standard paper size. * @default "A4" */ paperSize?: PaperSize; @@ -101,12 +226,12 @@ export interface MarkforgeConfig { margins?: PageMargins; /** - * Header template configuration. + * Running header template configuration. */ header?: HeaderFooterItem; /** - * Footer template configuration. + * Running footer template configuration. */ footer?: HeaderFooterItem; @@ -118,10 +243,57 @@ export interface MarkforgeConfig { /** * Optional watermark configuration to display across pages. - * By default, no watermark is displayed. + * @default false */ watermark?: string | WatermarkOptions | false; + /** + * Path(s) to custom CSS stylesheets to inject into the document. + */ + css?: string | string[]; + + /** + * Code syntax highlighting theme. + * @default "github-dark" + */ + syntaxTheme?: + | "github-dark" + | "github-light" + | "dracula" + | "monokai" + | "nord" + | SyntaxTheme + | (string & {}); +} + +/** + * Complete metadata extracted from Markdown frontmatter. + * Combines document metadata and layout overrides. + */ +export type FrontmatterMetadata = DocumentMetadata & DocumentLayoutConfig; + +/** + * Top-level MarkForge configuration (from config file, CLI, or API). + * Clean, 100% intuitive, and zero-duplicate architecture. + */ +export interface MarkforgeConfig extends DocumentLayoutConfig { + /** + * Target format(s) to compile markdown to. + * @default ["docx", "pdf"] + */ + to?: MarkforgeFormat | MarkforgeFormat[]; + + /** + * Output directory where compiled documents will be saved. + * Defaults to the same directory as the input file. + */ + outputDir?: string; + + /** + * Default document metadata (title, author, date, etc.) used when not provided in frontmatter. + */ + metadata?: DocumentMetadata; + /** * Whether to embed all images (local and remote) directly into the document. * @default true @@ -129,9 +301,10 @@ export interface MarkforgeConfig { embedImages?: boolean; /** - * Custom metadata overrides for title, author, date, etc. + * Whether to generate standalone self-contained HTML bundle. + * @default true */ - metadata?: Partial; + bundleHtml?: boolean; /** * Watch files and automatically recompile on changes. @@ -156,18 +329,6 @@ export interface MarkforgeConfig { * @default false */ open?: boolean; - - /** - * Whether to generate standalone self-contained HTML bundle. - * @default true - */ - bundleHtml?: boolean; - - /** - * Code syntax highlighting theme. - * @default "github-dark" - */ - syntaxTheme?: string; } export interface GeneratedOutputFile { diff --git a/packages/markforge/src/core/docx/docxBuilder.ts b/packages/markforge/src/core/docx/docxBuilder.ts index 3797392..75a756b 100644 --- a/packages/markforge/src/core/docx/docxBuilder.ts +++ b/packages/markforge/src/core/docx/docxBuilder.ts @@ -23,7 +23,7 @@ import { } from "docx"; import type { ParsedMarkdownDocument, MarkdownInlineSpan } from "../parser.js"; import { parseInlineSpans } from "../parser.js"; -import type { MarkforgeConfig } from "../../config/types.js"; +import type { MarkforgeConfig, ThemeProps } from "../../config/types.js"; import { resolveImage } from "../imageResolver.js"; import { tokenizeCodeLine } from "../syntax/syntaxHighlighter.js"; import { renderMermaidToPng } from "../mermaid/mermaidRenderer.js"; @@ -56,12 +56,23 @@ export function parseMarginToTwip(margin?: string | number, defaultTwip: number return isNaN(val) ? defaultTwip : Math.round(val); } +import { resolveDocumentConfig } from "../../config/resolveConfig.js"; + +export interface InlineRunOptions { + font?: string; + size?: number; + color?: string; + bold?: boolean; + italics?: boolean; +} + /** * Converts Markdown inline spans into Word TextRuns or Hyperlinks. */ export async function convertInlinesToTextRuns( spans: MarkdownInlineSpan[] = [], - baseDir: string = process.cwd() + baseDir: string = process.cwd(), + options: InlineRunOptions = {} ): Promise<(TextRun | ExternalHyperlink | ImageRun)[]> { const runs: (TextRun | ExternalHyperlink | ImageRun)[] = []; @@ -95,7 +106,10 @@ export async function convertInlinesToTextRuns( new TextRun({ text: span.content, style: "Hyperlink", - color: "0969DA", + color: "009DA0", + font: options.font, + size: options.size, + bold: options.bold, underline: {}, }), ], @@ -110,6 +124,10 @@ export async function convertInlinesToTextRuns( new TextRun({ text: span.content, bold: true, + font: options.font, + size: options.size, + color: options.color, + italics: options.italics, }) ); continue; @@ -120,6 +138,10 @@ export async function convertInlinesToTextRuns( new TextRun({ text: span.content, italics: true, + font: options.font, + size: options.size, + color: options.color, + bold: options.bold, }) ); continue; @@ -130,6 +152,9 @@ export async function convertInlinesToTextRuns( new TextRun({ text: span.content, strike: true, + font: options.font, + size: options.size, + color: options.color, }) ); continue; @@ -138,12 +163,13 @@ export async function convertInlinesToTextRuns( if (span.type === "code") { runs.push( new TextRun({ - text: ` ${span.content} `, + text: span.content, font: "Consolas", + size: options.size ? options.size - 2 : 19, + color: "0F172A", shading: { type: ShadingType.CLEAR, fill: "F1F5F9", - color: "0F172A", }, }) ); @@ -151,10 +177,10 @@ export async function convertInlinesToTextRuns( } if (span.type === "htmlInline") { - let colorHex: string | undefined; + let colorHex: string | undefined = options.color; let bgHex: string | undefined; - let isBold = false; - let isItalic = false; + let isBold = !!options.bold; + let isItalic = !!options.italics; if (span.style) { if (span.style.color) { @@ -172,13 +198,9 @@ export async function convertInlinesToTextRuns( } if (span.children && span.children.length > 0) { - const childRuns = await convertInlinesToTextRuns(span.children, baseDir); + const childRuns = await convertInlinesToTextRuns(span.children, baseDir, options); for (const child of childRuns) { - if (child instanceof TextRun) { - runs.push(child); - } else { - runs.push(child); - } + runs.push(child); } continue; } @@ -189,16 +211,23 @@ export async function convertInlinesToTextRuns( color: colorHex, bold: isBold, italics: isItalic, + font: options.font, + size: options.size, shading: bgHex ? { type: ShadingType.CLEAR, fill: bgHex } : undefined, }) ); continue; } - // Default plain text + // Plain text runs.push( new TextRun({ text: span.content, + font: options.font, + size: options.size, + color: options.color, + bold: options.bold, + italics: options.italics, }) ); } @@ -214,56 +243,77 @@ export async function buildDocxDocument( config: MarkforgeConfig, baseDir: string = process.cwd() ): Promise { - const metadata = { ...config.metadata, ...doc.metadata }; + const resolved = resolveDocumentConfig(doc.metadata as Record, config); const docElements: (Paragraph | Table)[] = []; - // 1. Cover / Title Area if metadata exists - if (metadata.title) { + // Theme Colors + const themeProps: ThemeProps = typeof resolved.theme === "object" ? resolved.theme : {}; + const primaryHex = (themeProps.primaryColor || "#33CDCF").replace("#", ""); + const primaryDarkHex = (themeProps.primaryDark || "#009DA0").replace("#", ""); + const textHex = (themeProps.textColor || "#0F172A").replace("#", ""); + const textMutedHex = (themeProps.textMuted || "#64748B").replace("#", ""); + const borderHex = (themeProps.borderColor || "#E2E8F0").replace("#", ""); + const cardBgHex = (themeProps.cardBackground || "#F8FAFC").replace("#", ""); + const defaultFont = themeProps.fontFamily + ? themeProps.fontFamily.split(",")[0].replace(/['"]/g, "").trim() + : "Segoe UI"; + + // 1. Cover / Title Area if title exists + if (resolved.title) { docElements.push( new Paragraph({ - text: metadata.title, - heading: HeadingLevel.TITLE, - spacing: { before: 200, after: 120 }, + children: [ + new TextRun({ + text: resolved.title, + bold: true, + size: 44, // 22pt + color: textHex, + font: defaultFont, + }), + ], + spacing: { before: 120, after: 80 }, }) ); - if (metadata.subtitle) { + if (resolved.subtitle) { docElements.push( new Paragraph({ children: [ new TextRun({ - text: metadata.subtitle, - italics: true, - color: "64748B", + text: resolved.subtitle, + color: textMutedHex, size: 24, // 12pt + font: defaultFont, }), ], - spacing: { after: 180 }, + spacing: { before: 60, after: 120 }, }) ); } - if (metadata.author || metadata.date) { + if (resolved.author || resolved.date || resolved.version || resolved.company) { const metaParts: string[] = []; - if (metadata.author) metaParts.push(`Author: ${Array.isArray(metadata.author) ? metadata.author.join(", ") : metadata.author}`); - if (metadata.date) metaParts.push(`Date: ${metadata.date}`); + if (resolved.author) metaParts.push(`Author: ${resolved.author}`); + if (resolved.version) metaParts.push(`Version: ${resolved.version}`); + if (resolved.date) metaParts.push(`Date: ${resolved.date}`); docElements.push( new Paragraph({ children: [ new TextRun({ - text: metaParts.join(" | "), - color: "94A3B8", - size: 20, // 10pt + text: metaParts.join(" "), + color: textMutedHex, + size: 18, // 9pt + font: defaultFont, }), ], - spacing: { after: 360 }, + spacing: { before: 40, after: 240 }, border: { bottom: { - color: "E2E8F0", - space: 10, + color: borderHex, + space: 12, style: BorderStyle.SINGLE, - size: 6, + size: 4, }, }, }) @@ -271,35 +321,165 @@ export async function buildDocxDocument( } } + // 1.5 Table of Contents (TOC) Card if enabled + if (resolved.toc) { + const headingNodes = doc.nodes.filter( + (n): n is typeof n & { type: "heading"; level: number; text: string } => + n.type === "heading" && typeof n.level === "number" && n.level >= 1 && n.level <= 3 + ); + + if (headingNodes.length > 0) { + const tocParagraphs: Paragraph[] = [ + new Paragraph({ + children: [ + new TextRun({ + text: "TABLE OF CONTENTS", + bold: true, + size: 18, // 9pt + color: textMutedHex, + font: defaultFont, + }), + ], + spacing: { after: 120 }, + border: { + bottom: { + color: borderHex, + space: 6, + style: BorderStyle.SINGLE, + size: 4, + }, + }, + }), + ]; + + for (const h of headingNodes) { + const indentLeft = (h.level - 1) * 240; + tocParagraphs.push( + new Paragraph({ + indent: { left: indentLeft }, + children: [ + new TextRun({ + text: h.text, + bold: h.level === 1, + color: primaryDarkHex, + size: h.level === 1 ? 21 : 20, + font: defaultFont, + }), + ], + spacing: { before: 20, after: 20 }, + }) + ); + } + + const tocCard = new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + columnWidths: [9000], + rows: [ + new TableRow({ + children: [ + new TableCell({ + width: { size: 9000, type: WidthType.DXA }, + shading: { fill: cardBgHex, type: ShadingType.CLEAR }, + margins: { top: 140, bottom: 140, left: 180, right: 180 }, + borders: { + top: { style: BorderStyle.SINGLE, size: 4, color: borderHex }, + bottom: { style: BorderStyle.SINGLE, size: 4, color: borderHex }, + left: { style: BorderStyle.SINGLE, size: 4, color: borderHex }, + right: { style: BorderStyle.SINGLE, size: 4, color: borderHex }, + }, + children: tocParagraphs, + }), + ], + }), + ], + }); + + docElements.push(tocCard); + docElements.push(new Paragraph({ spacing: { after: 200 } })); + } + } + // 2. Process AST Nodes for (const node of doc.nodes) { // Heading Nodes (H1-H6) if (node.type === "heading") { - let headingLevel: (typeof HeadingLevel)[keyof typeof HeadingLevel] = HeadingLevel.HEADING_1; - if (node.level === 2) headingLevel = HeadingLevel.HEADING_2; - if (node.level === 3) headingLevel = HeadingLevel.HEADING_3; - if (node.level === 4) headingLevel = HeadingLevel.HEADING_4; - if (node.level === 5) headingLevel = HeadingLevel.HEADING_5; - if (node.level === 6) headingLevel = HeadingLevel.HEADING_6; - - const runs = await convertInlinesToTextRuns(node.inlines, baseDir); - docElements.push( - new Paragraph({ - heading: headingLevel, - children: runs, - spacing: { before: 240, after: 120 }, - }) - ); + if (node.level === 1) { + const runs = await convertInlinesToTextRuns(node.inlines, baseDir, { + font: defaultFont, + size: 34, // 17pt + color: textHex, + bold: true, + }); + + docElements.push( + new Paragraph({ + heading: HeadingLevel.HEADING_1, + children: runs, + spacing: { before: 360, after: 140 }, + border: { + bottom: { + color: primaryHex, + space: 6, + style: BorderStyle.SINGLE, + size: 16, + }, + }, + }) + ); + } else if (node.level === 2) { + const runs = await convertInlinesToTextRuns(node.inlines, baseDir, { + font: defaultFont, + size: 26, // 13pt + color: primaryDarkHex, + bold: true, + }); + + docElements.push( + new Paragraph({ + heading: HeadingLevel.HEADING_2, + children: runs, + spacing: { before: 280, after: 100 }, + border: { + bottom: { + color: borderHex, + space: 4, + style: BorderStyle.SINGLE, + size: 4, + }, + }, + }) + ); + } else { + const runs = await convertInlinesToTextRuns(node.inlines, baseDir, { + font: defaultFont, + size: 22, // 11pt + color: textHex, + bold: true, + }); + + docElements.push( + new Paragraph({ + heading: node.level === 3 ? HeadingLevel.HEADING_3 : HeadingLevel.HEADING_4, + children: runs, + spacing: { before: 200, after: 80 }, + }) + ); + } continue; } // Paragraph Nodes if (node.type === "paragraph") { - const runs = await convertInlinesToTextRuns(node.inlines, baseDir); + const runs = await convertInlinesToTextRuns(node.inlines, baseDir, { + font: defaultFont, + size: 22, // 11pt + color: "334155", + }); + docElements.push( new Paragraph({ children: runs, - spacing: { before: 60, after: 140 }, + spacing: { before: 40, after: 140, line: 280 }, }) ); continue; @@ -379,7 +559,11 @@ export async function buildDocxDocument( title = "IMPORTANT"; } - const runs = await convertInlinesToTextRuns(node.inlines, baseDir); + const runs = await convertInlinesToTextRuns(node.inlines, baseDir, { + font: defaultFont, + size: 21, + color: "334155", + }); const calloutTable = new Table({ width: { size: 100, type: WidthType.PERCENTAGE }, @@ -395,7 +579,7 @@ export async function buildDocxDocument( top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE }, - left: { style: BorderStyle.SINGLE, size: 16, color: borderColor }, + left: { style: BorderStyle.SINGLE, size: 20, color: borderColor }, }, children: [ new Paragraph({ @@ -404,13 +588,15 @@ export async function buildDocxDocument( text: `[${title}]`, bold: true, color: borderColor, + font: defaultFont, size: 20, }), ], - spacing: { after: 60 }, + spacing: { after: 40 }, }), new Paragraph({ children: runs, + spacing: { line: 270 }, }), ], }), @@ -431,17 +617,27 @@ export async function buildDocxDocument( if (lines.length > 0) { for (const lineText of lines) { const spans = parseInlineSpans(lineText.replace(/^>\s?/, "").trim()); - const lineRuns = await convertInlinesToTextRuns(spans, baseDir); + const lineRuns = await convertInlinesToTextRuns(spans, baseDir, { + font: defaultFont, + size: 21, + color: "475569", + italics: true, + }); quoteParagraphs.push( new Paragraph({ children: lineRuns, - spacing: { before: 40, after: 40 }, + spacing: { before: 20, after: 20, line: 260 }, }) ); } } else { - const runs = await convertInlinesToTextRuns(node.inlines, baseDir); - quoteParagraphs.push(new Paragraph({ children: runs })); + const runs = await convertInlinesToTextRuns(node.inlines, baseDir, { + font: defaultFont, + size: 21, + color: "475569", + italics: true, + }); + quoteParagraphs.push(new Paragraph({ children: runs, spacing: { line: 260 } })); } const quoteTable = new Table({ @@ -458,7 +654,7 @@ export async function buildDocxDocument( top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE }, - left: { style: BorderStyle.SINGLE, size: 12, color: "33CDCF" }, + left: { style: BorderStyle.SINGLE, size: 16, color: primaryHex }, }, children: quoteParagraphs, }), @@ -496,7 +692,7 @@ export async function buildDocxDocument( docElements.push( new Paragraph({ children: [ - new TextRun({ text: "[Mermaid Diagram: " + (node.text || "").slice(0, 40) + "...]", bold: true, color: "33CDCF" }), + new TextRun({ text: "[Mermaid Diagram: " + (node.text || "").slice(0, 40) + "...]", bold: true, color: primaryHex }), ], spacing: { before: 60, after: 60 }, }) @@ -511,8 +707,12 @@ export async function buildDocxDocument( const numCols = node.children[0]?.children?.length || 1; const colWidth = Math.floor(9000 / numCols); - for (const rowNode of node.children) { + for (let rowIdx = 0; rowIdx < node.children.length; rowIdx++) { + const rowNode = node.children[rowIdx]; const cells: TableCell[] = []; + const isHeader = rowNode.isHeader; + const rowBg = isHeader ? "F1F5F9" : rowIdx % 2 === 0 ? "FFFFFF" : "F8FAFC"; + if (rowNode.children) { for (let colIdx = 0; colIdx < rowNode.children.length; colIdx++) { const cellNode = rowNode.children[colIdx]; @@ -521,25 +721,28 @@ export async function buildDocxDocument( if (align === "center") alignment = AlignmentType.CENTER; if (align === "right") alignment = AlignmentType.RIGHT; - const runs = await convertInlinesToTextRuns(cellNode.inlines, baseDir); + const runs = await convertInlinesToTextRuns(cellNode.inlines, baseDir, { + font: defaultFont, + size: 20, + color: isHeader ? "0F172A" : "334155", + bold: isHeader, + }); cells.push( new TableCell({ width: { size: colWidth, type: WidthType.DXA }, - shading: rowNode.isHeader ? { fill: "F1F5F9", type: ShadingType.CLEAR } : undefined, - margins: { top: 100, bottom: 100, left: 120, right: 120 }, + shading: { fill: rowBg, type: ShadingType.CLEAR }, + margins: { top: 100, bottom: 100, left: 140, right: 140 }, borders: { - top: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" }, - bottom: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" }, - left: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" }, - right: { style: BorderStyle.SINGLE, size: 4, color: "CBD5E1" }, + top: { style: BorderStyle.SINGLE, size: 4, color: "E2E8F0" }, + bottom: { style: BorderStyle.SINGLE, size: isHeader ? 8 : 4, color: isHeader ? "CBD5E1" : "E2E8F0" }, + left: { style: BorderStyle.SINGLE, size: 4, color: "E2E8F0" }, + right: { style: BorderStyle.SINGLE, size: 4, color: "E2E8F0" }, }, children: [ new Paragraph({ alignment, - children: rowNode.isHeader - ? runs.map((r) => (r instanceof TextRun ? new TextRun({ ...r, bold: true, color: "0F172A" }) : r)) - : runs, + children: runs, }), ], }) @@ -549,7 +752,7 @@ export async function buildDocxDocument( tableRows.push( new TableRow({ - tableHeader: rowNode.isHeader, + tableHeader: isHeader, children: cells, }) ); @@ -569,7 +772,11 @@ export async function buildDocxDocument( // List Item Nodes if (node.type === "list" && node.children) { for (const item of node.children) { - const runs = await convertInlinesToTextRuns(item.inlines, baseDir); + const runs = await convertInlinesToTextRuns(item.inlines, baseDir, { + font: defaultFont, + size: 21, + color: "334155", + }); let prefix = ""; if (item.checked !== undefined) { prefix = item.checked ? "[X] " : "[ ] "; @@ -582,7 +789,7 @@ export async function buildDocxDocument( ...(prefix ? [new TextRun({ text: prefix, bold: true, font: "Consolas" })] : []), ...runs, ], - spacing: { before: 40, after: 40 }, + spacing: { before: 20, after: 20, line: 260 }, }) ); } @@ -678,112 +885,220 @@ export async function buildDocxDocument( } } - // 3. Configure Header & Footer - const headerObj = metadata.header || config.header; - const footerObj = metadata.footer || config.footer; + // 3. Configure Header & Footer using native Word Tab Stops (0% table boundaries, 100% clean) + const contentWidthTwip = Math.max( + 1000, + resolved.paperDimensions.widthTwip - resolved.margins.leftTwip - resolved.margins.rightTwip + ); + const centerPos = Math.round(contentWidthTwip / 2); + const rightPos = contentWidthTwip; - // The standard Word header/footer layout uses two tab stops: - // one center tab at the middle of the page, one right tab at the end. - // We use 9026 (half of ~18000 twip usable width) for center and 9026*2 for right. - const headerTabStops = [ - { type: TabStopType.CENTER, position: 4513 }, - { type: TabStopType.RIGHT, position: 9026 }, - ]; + const headerRuns: TextRun[] = []; + if (resolved.header?.left) { + headerRuns.push( + new TextRun({ + text: resolved.header.left.text, + color: resolved.header.left.color.replace("#", ""), + size: (resolved.header.left.fontSize || 9) * 2, + font: resolved.header.left.fontFamily || defaultFont, + bold: resolved.header.left.bold, + italics: resolved.header.left.italic, + }) + ); + } + headerRuns.push(new TextRun({ text: "\t" })); + if (resolved.header?.center) { + headerRuns.push( + new TextRun({ + text: resolved.header.center.text, + color: resolved.header.center.color.replace("#", ""), + size: (resolved.header.center.fontSize || 9) * 2, + font: resolved.header.center.fontFamily || defaultFont, + bold: resolved.header.center.bold, + italics: resolved.header.center.italic, + }) + ); + } + headerRuns.push(new TextRun({ text: "\t" })); + if (resolved.header?.right) { + headerRuns.push( + new TextRun({ + text: resolved.header.right.text, + color: resolved.header.right.color.replace("#", ""), + size: (resolved.header.right.fontSize || 9) * 2, + font: resolved.header.right.fontFamily || defaultFont, + bold: resolved.header.right.bold, + italics: resolved.header.right.italic, + }) + ); + } - const docHeader = headerObj + const docHeader = resolved.header ? new Header({ children: [ new Paragraph({ - tabStops: headerTabStops, - children: [ - // Left zone - ...(headerObj.left - ? [ - new TextRun({ - text: headerObj.left.replace("{title}", metadata.title || ""), - color: "94A3B8", - size: 18, - }), - ] - : []), - // Center zone (tab + text) - ...(headerObj.center - ? [ - new TextRun({ text: "\t", color: "94A3B8", size: 18 }), - new TextRun({ - text: headerObj.center.replace("{title}", metadata.title || ""), - color: "94A3B8", - size: 18, - }), - ] - : []), - // Right zone (tab + text) β€” skip extra tab if center already used one - ...(headerObj.right - ? [ - new TextRun({ - text: (headerObj.left || headerObj.center) ? "\t" : "", - color: "94A3B8", - size: 18, - }), - new TextRun({ - text: headerObj.right.replace("{title}", metadata.title || ""), - color: "94A3B8", - size: 18, - }), - ] - : []), + tabStops: [ + { + type: TabStopType.CENTER, + position: centerPos, + }, + { + type: TabStopType.RIGHT, + position: rightPos, + }, ], + border: resolved.header.divider + ? { + bottom: { + style: BorderStyle.SINGLE, + size: 4, + space: 6, + color: (resolved.header.dividerColor || "#CBD5E1").replace("#", ""), + }, + } + : undefined, + children: headerRuns, + spacing: { after: 120 }, }), ], }) : undefined; - const docFooter = footerObj + const footerRuns: TextRun[] = []; + if (resolved.footer?.left) { + footerRuns.push( + new TextRun({ + text: resolved.footer.left.text, + color: resolved.footer.left.color.replace("#", ""), + size: (resolved.footer.left.fontSize || 9) * 2, + font: resolved.footer.left.fontFamily || defaultFont, + bold: resolved.footer.left.bold, + italics: resolved.footer.left.italic, + }) + ); + } + footerRuns.push(new TextRun({ text: "\t" })); + if (resolved.footer?.center) { + footerRuns.push( + new TextRun({ + text: resolved.footer.center.text, + color: resolved.footer.center.color.replace("#", ""), + size: (resolved.footer.center.fontSize || 9) * 2, + font: resolved.footer.center.fontFamily || defaultFont, + bold: resolved.footer.center.bold, + italics: resolved.footer.center.italic, + }) + ); + } + footerRuns.push(new TextRun({ text: "\t" })); + if (resolved.footer?.right) { + const rZone = resolved.footer.right; + const rColor = rZone.color.replace("#", ""); + const rSize = (rZone.fontSize || 9) * 2; + const rFont = rZone.fontFamily || defaultFont; + const rBold = rZone.bold; + const rItalics = rZone.italic; + + if (rZone.text.includes("{page}") || rZone.text.includes("{pages}")) { + const parts = rZone.text.split(/(\{page\}|\{pages\})/gi); + for (const part of parts) { + if (part.toLowerCase() === "{page}") { + footerRuns.push( + new TextRun({ + children: [PageNumber.CURRENT], + color: rColor, + size: rSize, + font: rFont, + bold: rBold, + italics: rItalics, + }) + ); + } else if (part.toLowerCase() === "{pages}") { + footerRuns.push( + new TextRun({ + children: [PageNumber.TOTAL_PAGES], + color: rColor, + size: rSize, + font: rFont, + bold: rBold, + italics: rItalics, + }) + ); + } else if (part) { + footerRuns.push( + new TextRun({ + text: part, + color: rColor, + size: rSize, + font: rFont, + bold: rBold, + italics: rItalics, + }) + ); + } + } + } else { + footerRuns.push( + new TextRun({ + text: rZone.text, + color: rColor, + size: rSize, + font: rFont, + bold: rBold, + italics: rItalics, + }) + ); + } + } + + const docFooter = resolved.footer ? new Footer({ children: [ new Paragraph({ - tabStops: headerTabStops, - children: [ - // Left zone - ...(footerObj.left - ? [ - new TextRun({ - text: footerObj.left - .replace("{page}", "") - .replace("{pages}", "") - .trim(), - color: "94A3B8", - size: 18, - }), - ] - : []), - // Right zone: always includes page number if right is configured or footer exists - new TextRun({ text: "\t", color: "94A3B8", size: 18 }), - new TextRun({ text: "Page ", color: "94A3B8", size: 18 }), - new TextRun({ children: [PageNumber.CURRENT], color: "94A3B8", size: 18 }), - new TextRun({ text: " of ", color: "94A3B8", size: 18 }), - new TextRun({ children: [PageNumber.TOTAL_PAGES], color: "94A3B8", size: 18 }), + tabStops: [ + { + type: TabStopType.CENTER, + position: centerPos, + }, + { + type: TabStopType.RIGHT, + position: rightPos, + }, ], + border: resolved.footer.divider + ? { + top: { + style: BorderStyle.SINGLE, + size: 4, + space: 6, + color: (resolved.footer.dividerColor || "#CBD5E1").replace("#", ""), + }, + } + : undefined, + children: footerRuns, + spacing: { before: 120 }, }), ], }) : undefined; - // 4. Page Margins & Orientation - const topMargin = parseMarginToTwip(metadata.margins?.top || config.margins?.top, 1440); - const bottomMargin = parseMarginToTwip(metadata.margins?.bottom || config.margins?.bottom, 1440); - const leftMargin = parseMarginToTwip(metadata.margins?.left || config.margins?.left, 1440); - const rightMargin = parseMarginToTwip(metadata.margins?.right || config.margins?.right, 1440); - const isLandscape = (metadata.orientation || config.orientation) === "landscape"; + // 4. Page Dimensions & Margins + const isLandscape = resolved.orientation === "landscape"; const document = new Document({ styles: { default: { document: { run: { - font: "Segoe UI", - size: 22, // 11pt - color: "0F172A", + font: defaultFont, + size: 21, // 10.5pt + color: textHex, + }, + paragraph: { + spacing: { + line: 276, // 1.15 line spacing + after: 140, + }, }, }, }, @@ -793,13 +1108,15 @@ export async function buildDocxDocument( properties: { page: { size: { + width: resolved.paperDimensions.widthTwip, + height: resolved.paperDimensions.heightTwip, orientation: isLandscape ? PageOrientation.LANDSCAPE : PageOrientation.PORTRAIT, }, margin: { - top: topMargin, - bottom: bottomMargin, - left: leftMargin, - right: rightMargin, + top: resolved.margins.topTwip, + bottom: resolved.margins.bottomTwip, + left: resolved.margins.leftTwip, + right: resolved.margins.rightTwip, header: 720, footer: 720, }, diff --git a/packages/markforge/src/core/html/htmlBuilder.ts b/packages/markforge/src/core/html/htmlBuilder.ts index 3c29963..2b6b2f1 100644 --- a/packages/markforge/src/core/html/htmlBuilder.ts +++ b/packages/markforge/src/core/html/htmlBuilder.ts @@ -3,8 +3,9 @@ import * as path from "node:path"; import type { ParsedMarkdownDocument, MarkdownInlineSpan } from "../parser.js"; import type { MarkforgeConfig } from "../../config/types.js"; import { resolveImage } from "../imageResolver.js"; -import { THEMES, THEME_COMPONENTS } from "./htmlThemes.js"; +import { generateThemeCss, THEME_COMPONENTS } from "./htmlThemes.js"; import { highlightCodeToHtml } from "../syntax/syntaxHighlighter.js"; +import { resolveDocumentConfig } from "../../config/resolveConfig.js"; /** * Escapes HTML characters safely. @@ -89,19 +90,15 @@ export async function buildHtmlDocument( config: MarkforgeConfig, baseDir: string = process.cwd() ): Promise { - const metadata = { ...config.metadata, ...doc.metadata }; - const themeName = metadata.theme || config.theme || "default"; - const baseThemeCss = THEMES[themeName] || THEMES.default; + const resolved = resolveDocumentConfig(doc.metadata as Record, config); + const baseThemeCss = generateThemeCss(resolved.theme); // Load custom external CSS if provided let customCss = ""; - if (config.css) { - const cssList = Array.isArray(config.css) ? config.css : [config.css]; - for (const cssPath of cssList) { - const fullCssPath = path.isAbsolute(cssPath) ? cssPath : path.resolve(baseDir, cssPath); - if (fs.existsSync(fullCssPath)) { - customCss += `\n/* Custom CSS: ${cssPath} */\n` + fs.readFileSync(fullCssPath, "utf-8"); - } + for (const cssPath of resolved.css) { + const fullCssPath = path.isAbsolute(cssPath) ? cssPath : path.resolve(baseDir, cssPath); + if (fs.existsSync(fullCssPath)) { + customCss += `\n/* Custom CSS: ${cssPath} */\n` + fs.readFileSync(fullCssPath, "utf-8"); } } @@ -110,21 +107,23 @@ export async function buildHtmlDocument( let bodyHtml = ""; - // 1. Header Area if metadata exists - if (metadata.title) { + // 1. Header Area if title exists + if (resolved.title) { bodyHtml += `

\n`; - bodyHtml += `

${escapeHtml(metadata.title)}

\n`; - if (metadata.subtitle) { - bodyHtml += `
${escapeHtml(metadata.subtitle)}
\n`; + bodyHtml += `

${escapeHtml(resolved.title)}

\n`; + if (resolved.subtitle) { + bodyHtml += `
${escapeHtml(resolved.subtitle)}
\n`; } - if (metadata.author || metadata.date) { + if (resolved.author || resolved.date || resolved.version) { bodyHtml += `
\n`; - if (metadata.author) { - const authors = Array.isArray(metadata.author) ? metadata.author.join(", ") : metadata.author; - bodyHtml += ` Author: ${escapeHtml(authors)}\n`; + if (resolved.author) { + bodyHtml += ` Author: ${escapeHtml(resolved.author)}\n`; + } + if (resolved.version) { + bodyHtml += ` Version: ${escapeHtml(resolved.version)}\n`; } - if (metadata.date) { - bodyHtml += ` Date: ${escapeHtml(metadata.date)}\n`; + if (resolved.date) { + bodyHtml += ` Date: ${escapeHtml(resolved.date)}\n`; } bodyHtml += `
\n`; } @@ -132,16 +131,14 @@ export async function buildHtmlDocument( } // 2. Table of Contents if enabled - if (config.toc || metadata.toc) { - if (doc.tocEntries.length > 0) { - bodyHtml += ` \n`; + if (resolved.toc && doc.tocEntries.length > 0) { + bodyHtml += ` \n`; } // 3. Render AST Nodes @@ -161,7 +158,7 @@ export async function buildHtmlDocument( if (node.type === "codeBlock") { const lang = node.language || ""; const langClass = lang ? ` class="language-${escapeHtml(lang)}"` : ""; - const highlighted = highlightCodeToHtml(node.text || "", lang); + const highlighted = highlightCodeToHtml(node.text || "", lang, resolved.syntaxTheme); bodyHtml += `
${highlighted}
\n`; continue; } @@ -199,14 +196,11 @@ export async function buildHtmlDocument( bodyHtml += ` \n`; for (const row of node.children) { bodyHtml += ` \n`; - if (row.children) { - for (let colIdx = 0; colIdx < row.children.length; colIdx++) { - const cell = row.children[colIdx]; - const tag = row.isHeader ? "th" : "td"; - const align = node.align?.[colIdx] ? ` style="text-align: ${node.align[colIdx]}"` : ""; - const cellInner = await renderInlinesToHtml(cell.inlines, baseDir); - bodyHtml += ` <${tag}${align}>${cellInner}\n`; - } + for (const cell of row.children || []) { + const tag = cell.isHeader ? "th" : "td"; + const align = cell.align ? ` align="${cell.align}"` : ""; + const inner = await renderInlinesToHtml(cell.inlines, baseDir); + bodyHtml += ` <${tag}${align}>${inner}\n`; } bodyHtml += ` \n`; } @@ -218,58 +212,52 @@ export async function buildHtmlDocument( const tag = node.ordered ? "ol" : "ul"; bodyHtml += ` <${tag}>\n`; for (const item of node.children) { - const itemInner = await renderInlinesToHtml(item.inlines, baseDir); - let prefix = ""; - if (item.checked !== undefined) { - prefix = ` `; - } - bodyHtml += `
  • ${prefix}${itemInner}
  • \n`; + const inner = await renderInlinesToHtml(item.inlines, baseDir); + bodyHtml += `
  • ${inner}
  • \n`; } bodyHtml += ` \n`; continue; } - if (node.type === "htmlBlock") { - bodyHtml += ` ${node.rawHtml}\n`; + if (node.type === "thematicBreak") { + bodyHtml += `
    \n`; continue; } - if (node.type === "thematicBreak") { - bodyHtml += `
    \n`; + if (node.type === "htmlBlock") { + bodyHtml += ` ${node.text}\n`; continue; } } - // Watermark support (only when explicitly configured) - const wmConfig = config.watermark ?? metadata.watermark; - let watermarkHtml = ""; + // 4. Watermark if enabled let watermarkCss = ""; - if (wmConfig) { - const wmText = typeof wmConfig === "string" ? wmConfig : wmConfig.text; - const opacity = typeof wmConfig === "object" && wmConfig.opacity !== undefined ? wmConfig.opacity : 0.12; - const rotate = typeof wmConfig === "object" && wmConfig.rotate !== undefined ? wmConfig.rotate : -45; - const color = typeof wmConfig === "object" && wmConfig.color ? wmConfig.color : "#94A3B8"; - const fontSize = typeof wmConfig === "object" && wmConfig.fontSize ? `${wmConfig.fontSize}pt` : "52pt"; - + let watermarkHtml = ""; + if (resolved.watermark) { + const wm = resolved.watermark; watermarkCss = ` .document-watermark { position: fixed; top: 50%; left: 50%; - transform: translate(-50%, -50%) rotate(${rotate}deg); - font-size: ${fontSize}; - font-weight: 800; - color: ${color}; - opacity: ${opacity}; + transform: translate(-50%, -50%) rotate(${wm.rotate}deg); + font-size: ${wm.fontSize}pt; + font-weight: 900; + color: ${wm.color}; + opacity: ${wm.opacity}; pointer-events: none; + z-index: 0; user-select: none; - z-index: 9999; text-transform: uppercase; letter-spacing: 0.15em; white-space: nowrap; } + .document-container { + position: relative; + z-index: 1; + } `; - watermarkHtml = `
    ${escapeHtml(wmText)}
    \n`; + watermarkHtml = `
    ${escapeHtml(wm.text)}
    \n`; } const hasMermaid = doc.nodes.some((n) => n.type === "mermaid"); @@ -291,14 +279,12 @@ export async function buildHtmlDocument( ` : ""; - const documentTitle = metadata.title || "MarkForge Document"; - return ` - + - ${escapeHtml(documentTitle)} + ${escapeHtml(resolved.title)}`); } + /** * Generates a minimal valid PDF-1.4 binary buffer fallback. */ @@ -209,6 +236,35 @@ export async function buildPdfDocument( const tmpDir = os.tmpdir(); const tmpHtml = path.join(tmpDir, `markforge_${tmpId}.html`); const tmpPdf = path.join(tmpDir, `markforge_${tmpId}.pdf`); + const tmpProfile = path.join(tmpDir, `markforge_prof_${tmpId}`); + + // Enterprise-isolated flags: Prevents Chrome/Edge from accessing user Google accounts, syncing, or sending telemetry + const isolatedFlags = [ + `--user-data-dir=${tmpProfile}`, + "--no-first-run", + "--no-default-browser-check", + "--disable-sync", + "--disable-background-networking", + "--disable-component-update", + "--disable-default-apps", + "--disable-extensions", + "--disable-domain-reliability", + "--disable-client-side-phishing-detection", + "--disable-breakpad", + "--disable-component-extensions-with-background-pages", + "--disable-features=Translate,OptimizationHints,MediaRouter,DialMediaRouteProvider,CalculatedNewTabPage,ChromeWhatsNewUI,PrivacySandboxSettings4", + "--password-store=basic", + "--use-mock-keychain", + "--mute-audio", + "--no-service-autorun", + "--disable-gpu", + "--no-sandbox", + "--disable-setuid-sandbox", + "--allow-file-access-from-files", + "--disable-web-security", + "--force-color-profile=srgb", + "--no-pdf-header-footer", + ]; try { fs.writeFileSync(tmpHtml, pagedHtml, "utf-8"); @@ -218,15 +274,9 @@ export async function buildPdfDocument( chromePath, [ "--headless=new", - "--disable-gpu", - "--no-sandbox", - "--disable-setuid-sandbox", - "--allow-file-access-from-files", - "--disable-web-security", - "--force-color-profile=srgb", + ...isolatedFlags, "--run-all-compositor-stages-before-draw", "--virtual-time-budget=8000", - "--no-pdf-header-footer", `--print-to-pdf=${tmpPdf}`, fileUrl, ], @@ -239,13 +289,7 @@ export async function buildPdfDocument( chromePath, [ "--headless", - "--disable-gpu", - "--no-sandbox", - "--disable-setuid-sandbox", - "--allow-file-access-from-files", - "--disable-web-security", - "--force-color-profile=srgb", - "--no-pdf-header-footer", + ...isolatedFlags, `--print-to-pdf=${tmpPdf}`, fileUrl, ], @@ -263,6 +307,7 @@ export async function buildPdfDocument( try { if (fs.existsSync(tmpHtml)) fs.unlinkSync(tmpHtml); if (fs.existsSync(tmpPdf)) fs.unlinkSync(tmpPdf); + if (fs.existsSync(tmpProfile)) fs.rmSync(tmpProfile, { recursive: true, force: true }); } catch { // ignore cleanup errors } diff --git a/packages/markforge/src/core/syntax/syntaxHighlighter.ts b/packages/markforge/src/core/syntax/syntaxHighlighter.ts index efae83b..173af3a 100644 --- a/packages/markforge/src/core/syntax/syntaxHighlighter.ts +++ b/packages/markforge/src/core/syntax/syntaxHighlighter.ts @@ -34,10 +34,6 @@ export const SYNTAX_COLORS = { plain: "E2E8F0", // Light gray β€” identifiers / plain text }; -/** - * Light-background palette β€” for DOCX (white/light code block backgrounds). - * Based on GitHub Light / VS Code Light+ theme. - */ export const SYNTAX_COLORS_LIGHT = { keyword: "D73A49", // Crimson red β€” keywords string: "0A7E5C", // Forest teal β€” strings (readable on white) @@ -51,6 +47,54 @@ export const SYNTAX_COLORS_LIGHT = { plain: "24292E", // Near-black β€” identifiers }; +export const SYNTAX_THEMES: Record = { + "github-dark": SYNTAX_COLORS, + "dark": SYNTAX_COLORS, + "github-light": SYNTAX_COLORS_LIGHT, + "light": SYNTAX_COLORS_LIGHT, + "dracula": { + keyword: "FF79C6", + string: "F1FA8C", + comment: "6272A4", + number: "BD93F9", + boolean: "BD93F9", + function: "50FA7B", + type: "8BE9FD", + operator: "FF79C6", + punctuation: "F8F8F2", + plain: "F8F8F2", + }, + "monokai": { + keyword: "F92672", + string: "E6DB74", + comment: "75715E", + number: "AE81FF", + boolean: "AE81FF", + function: "A6E22E", + type: "66D9EF", + operator: "F92672", + punctuation: "F8F8F2", + plain: "F8F8F2", + }, + "nord": { + keyword: "81A1C1", + string: "A3BE8C", + comment: "616E88", + number: "B48EAD", + boolean: "81A1C1", + function: "88C0D0", + type: "8FBCBB", + operator: "81A1C1", + punctuation: "ECEFF4", + plain: "D8DEE9", + }, +}; + +export function getSyntaxPalette(themeName: string = "github-dark"): typeof SYNTAX_COLORS { + return SYNTAX_THEMES[themeName.toLowerCase()] || SYNTAX_THEMES["github-dark"]; +} + + const JS_KEYWORDS = new Set([ "import", "from", "export", "default", "const", "let", "var", "function", "async", "await", "return", "if", "else", "for", "while", "switch", "case", @@ -79,11 +123,12 @@ const SQL_KEYWORDS = new Set([ "and", "or", "not", "null", "primary", "key", "foreign", "references" ]); -/** - * Tokenizes a single line of code into styled tokens based on language. - */ -export function tokenizeCodeLine(line: string, lang: string = "", theme: "dark" | "light" = "dark"): SyntaxToken[] { - const COLORS = theme === "light" ? SYNTAX_COLORS_LIGHT : SYNTAX_COLORS; +export function tokenizeCodeLine( + line: string, + lang: string = "", + theme: string = "github-dark" +): SyntaxToken[] { + const COLORS = getSyntaxPalette(theme); if (!line) { return [{ text: " ", type: "plain", colorHex: COLORS.plain }]; } @@ -206,6 +251,12 @@ export function tokenizeCodeLine(line: string, lang: string = "", theme: "dark" continue; } + // Check Types / Classes (starts with Capital letter) + if (/^[A-Z][a-zA-Z0-9_$]*$/.test(word)) { + tokens.push({ text: word, type: "type", colorHex: COLORS.type }); + continue; + } + // Check function call: word followed by ( let nextNonWs = pos; while (nextNonWs < line.length && /\s/.test(line[nextNonWs])) { @@ -216,12 +267,6 @@ export function tokenizeCodeLine(line: string, lang: string = "", theme: "dark" continue; } - // Check Types / Classes (starts with Capital letter) - if (/^[A-Z][a-zA-Z0-9_$]*$/.test(word)) { - tokens.push({ text: word, type: "type", colorHex: COLORS.type }); - continue; - } - // Plain identifier tokens.push({ text: word, type: "plain", colorHex: COLORS.plain }); continue; @@ -242,10 +287,14 @@ export function tokenizeCodeLine(line: string, lang: string = "", theme: "dark" /** * Converts a code snippet to syntax-highlighted HTML with colored span tokens. */ -export function highlightCodeToHtml(code: string, lang: string = ""): string { +export function highlightCodeToHtml( + code: string, + lang: string = "", + theme: string = "github-dark" +): string { const lines = (code || "").split("\n"); const htmlLines = lines.map((line) => { - const tokens = tokenizeCodeLine(line, lang); + const tokens = tokenizeCodeLine(line, lang, theme); return tokens .map((t) => { const escaped = t.text diff --git a/packages/markforge/src/index.ts b/packages/markforge/src/index.ts index 568c4ea..9df4444 100644 --- a/packages/markforge/src/index.ts +++ b/packages/markforge/src/index.ts @@ -15,11 +15,36 @@ export type { ResolvedImage } from "./core/imageResolver.js"; export { tokenizeCodeLine, highlightCodeToHtml, SYNTAX_COLORS } from "./core/syntax/syntaxHighlighter.js"; export type { SyntaxToken } from "./core/syntax/syntaxHighlighter.js"; export { renderMermaidToPng } from "./core/mermaid/mermaidRenderer.js"; -export { THEMES, THEME_DEFAULT, THEME_ACADEMIC } from "./core/html/htmlThemes.js"; +export { THEMES, THEME_DEFAULT, THEME_CORPORATE, generateThemeCss } from "./core/html/htmlThemes.js"; export { defineConfig } from "./config/defineConfig.js"; export { loadConfig, DEFAULT_CONFIG } from "./config/loadConfig.js"; +export { + resolveDocumentConfig, + replaceDocumentTokens, + normalizeWatermark, + normalizeHeaderFooter, + normalizeHeaderFooterSlot, + PAPER_DIMENSIONS_TWIP, +} from "./config/resolveConfig.js"; export type { + ResolvedDocumentConfig, + NormalizedMargins, + NormalizedWatermark, + NormalizedHeaderFooter, + NormalizedHeaderFooterZone, +} from "./config/resolveConfig.js"; +export { + OutputFormat, + Theme, + Orientation, + PaperSizeEnum, + SyntaxTheme, + WatermarkPosition, +} from "./config/types.js"; +export type { + ThemeProps, + HeaderFooterSlot, MarkforgeConfig, MarkforgeFormat, MarkforgeTheme, @@ -27,6 +52,8 @@ export type { PaperSize, PageMargins, HeaderFooterItem, + DocumentMetadata, + DocumentLayoutConfig, FrontmatterMetadata, WatermarkOptions, GeneratedOutputFile, diff --git a/packages/markforge/src/ui/Header.tsx b/packages/markforge/src/ui/Header.tsx index f6741a8..02dae70 100644 --- a/packages/markforge/src/ui/Header.tsx +++ b/packages/markforge/src/ui/Header.tsx @@ -2,12 +2,16 @@ import React from "react"; import { Box, Text } from "ink"; import { MARKFORGE_VERSION } from "../version.js"; +import type { MarkforgeTheme } from "../config/types.js"; + export interface HeaderProps { inputFile?: string; - theme?: string; + theme?: MarkforgeTheme; } -export const Header: React.FC = ({ inputFile, theme = "default" }) => { +export const Header: React.FC = ({ inputFile, theme = "corporate" }) => { + const themeLabel = typeof theme === "object" ? "custom (ThemeProps)" : String(theme || "corporate"); + return ( = ({ inputFile, theme = "default" }) )} Theme: - {theme} + {themeLabel} diff --git a/packages/markforge/src/version.ts b/packages/markforge/src/version.ts index 3a1c276..bbb4f5b 100644 --- a/packages/markforge/src/version.ts +++ b/packages/markforge/src/version.ts @@ -73,7 +73,9 @@ function getPackageDir(): string { */ export const MARKFORGE_VERSION: string = readVersionFromPackageJson(getPackageDir()); -/** @deprecated Use MARKFORGE_VERSION directly. */ +/** + * Resolves the MarkForge version dynamically from package.json in the specified or current directory. + */ export function getMarkforgeVersion(fromDir: string = getPackageDir()): string { return readVersionFromPackageJson(fromDir); } diff --git a/packages/markforge/test/allProperties.test.ts b/packages/markforge/test/allProperties.test.ts new file mode 100644 index 0000000..4c6cab1 --- /dev/null +++ b/packages/markforge/test/allProperties.test.ts @@ -0,0 +1,259 @@ +import { describe, it, expect } from "bun:test"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import { + compileMarkdown, + parseMarkdownDocument, + buildHtmlDocument, + buildDocxDocument, + buildPdfDocument, + resolveDocumentConfig, + replaceDocumentTokens, + normalizeWatermark, + PAPER_DIMENSIONS_TWIP, +} from "../src/index.js"; +import type { MarkforgeConfig } from "../src/config/types.js"; + +describe("All Properties Comprehensive Test Suite", () => { + const tmpDir = path.resolve(process.cwd(), ".temp/all-props-test"); + if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true }); + + const customCssPath = path.join(tmpDir, "custom.css"); + fs.writeFileSync( + customCssPath, + ".custom-brand { color: #33CDCF; font-weight: bold; }\n" + ); + + const fullMarkdown = `--- +title: "Unified Platform Architecture" +subtitle: "High-Performance Document Engine" +author: "Masum RPG" +date: "2026-08-29" +version: "1.0.0" +company: "Masum Dev" +lang: "en" +theme: "corporate" +orientation: "landscape" +paperSize: "A4" +margins: + top: "3cm" + bottom: "3cm" + left: "2cm" + right: "2cm" +header: + left: "{company} - {title}" + center: "Confidential Manual" + right: "v{version}" +footer: + left: "Author: {author}" + right: "Page {page} of {pages}" +toc: true +watermark: + text: "STRICTLY CONFIDENTIAL" + color: "#E11D48" + opacity: 0.15 + fontSize: 48 + rotate: -35 + position: "diagonal" +css: + - "${customCssPath}" +--- + +# Architecture Overview + +Welcome to the MarkForge full-specification verification document. + +
    Verified with Custom Brand CSS
    + +## Core Pipeline + +This section tests table of contents indexing, syntax themes, callouts, and tables. + +> [!NOTE] +> This is a high-priority informational note. + +> [!IMPORTANT] +> Verify that all custom header tokens and watermarks are rendered properly. + +\`\`\`typescript +interface EngineConfig { + theme: string; + watermark: boolean | object; +} + +export function initializeEngine(): EngineConfig { + return { theme: "corporate", watermark: true }; +} +\`\`\` + +| Component | Status | Latency | +| :--- | :---: | ---: | +| DOCX Builder | Verified | 45ms | +| PDF Renderer | Verified | 210ms | +| HTML Compiler | Verified | 12ms | +`; + + it("1. resolveDocumentConfig verifies 100% of all properties and token replacement", () => { + const doc = parseMarkdownDocument(fullMarkdown); + const globalConfig: MarkforgeConfig = { + to: ["docx", "pdf", "html"], + syntaxTheme: "dracula", + bundleHtml: true, + embedImages: true, + metadata: { + company: "Masum Dev Global", + }, + }; + + const resolved = resolveDocumentConfig(doc.metadata as Record, globalConfig); + + // Metadata & Dynamic tokens + expect(resolved.title).toBe("Unified Platform Architecture"); + expect(resolved.subtitle).toBe("High-Performance Document Engine"); + expect(resolved.author).toBe("Masum RPG"); + expect(resolved.date).toBe("2026-08-29"); + expect(resolved.version).toBe("1.0.0"); + expect(resolved.company).toBe("Masum Dev"); + expect(resolved.lang).toBe("en"); + + // Theme & Layout + expect(resolved.theme).toBe("corporate"); + expect(resolved.orientation).toBe("landscape"); + expect(resolved.paperSize).toBe("A4"); + expect(resolved.paperDimensions.widthTwip).toBe(PAPER_DIMENSIONS_TWIP.A4.height); // landscape swapped + expect(resolved.paperDimensions.heightTwip).toBe(PAPER_DIMENSIONS_TWIP.A4.width); + + // Margins (Normalized CSS & Twip) + expect(resolved.margins.top).toBe("3cm"); + expect(resolved.margins.bottom).toBe("3cm"); + expect(resolved.margins.left).toBe("2cm"); + expect(resolved.margins.right).toBe("2cm"); + expect(resolved.margins.topTwip).toBe(Math.round(3 * 566.929)); + expect(resolved.margins.leftTwip).toBe(Math.round(2 * 566.929)); + + // Headers with replaced tokens + expect(resolved.header?.left?.text).toBe("Masum Dev - Unified Platform Architecture"); + expect(resolved.header?.center?.text).toBe("Confidential Manual"); + expect(resolved.header?.right?.text).toBe("v1.0.0"); + + // Footers with replaced tokens + expect(resolved.footer?.left?.text).toBe("Author: Masum RPG"); + + // Table of Contents & Watermark + expect(resolved.toc).toBe(true); + expect(resolved.watermark).toBeDefined(); + expect(resolved.watermark?.text).toBe("STRICTLY CONFIDENTIAL"); + expect(resolved.watermark?.color).toBe("#E11D48"); + expect(resolved.watermark?.opacity).toBe(0.15); + expect(resolved.watermark?.fontSize).toBe(48); + expect(resolved.watermark?.rotate).toBe(-35); + + // CSS list & Engine Flags + expect(resolved.css).toContain(customCssPath); + expect(resolved.syntaxTheme).toBe("dracula"); + expect(resolved.embedImages).toBe(true); + expect(resolved.bundleHtml).toBe(true); + }); + + it("2. buildHtmlDocument applies all visual properties (TOC, Watermark, Custom CSS, Syntax theme, Meta)", async () => { + const doc = parseMarkdownDocument(fullMarkdown); + const config: MarkforgeConfig = { + theme: "corporate", + syntaxTheme: "dracula", + }; + + const html = await buildHtmlDocument(doc, config, tmpDir); + + // Document structure & language + expect(html).toContain('lang="en"'); + expect(html).toContain("Unified Platform Architecture"); + + // Header metadata area + expect(html).toContain('

    Unified Platform Architecture

    '); + expect(html).toContain('
    High-Performance Document Engine
    '); + expect(html).toContain("Author: Masum RPG"); + expect(html).toContain("Version: 1.0.0"); + expect(html).toContain("Date: 2026-08-29"); + + // Table of Contents + expect(html).toContain('