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 0000000..906bb67 Binary files /dev/null and b/packages/markforge/document.docx differ diff --git a/packages/markforge/document.pdf b/packages/markforge/document.pdf new file mode 100644 index 0000000..6c277b7 Binary files /dev/null and b/packages/markforge/document.pdf differ 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('