diff --git a/apps/cli/markforge/assets/company-logo.png b/apps/cli/markforge/assets/company-logo.png new file mode 100644 index 0000000..ff9f5a8 Binary files /dev/null and b/apps/cli/markforge/assets/company-logo.png differ diff --git a/apps/cli/markforge/docs/full-all-properties.md b/apps/cli/markforge/docs/full-all-properties.md index b760f18..730fc54 100644 --- a/apps/cli/markforge/docs/full-all-properties.md +++ b/apps/cli/markforge/docs/full-all-properties.md @@ -76,7 +76,49 @@ def check_environment(): > Use `--watch` flag during local authoring for instant live updates. > [!IMPORTANT] -> Both Word Twips and CSS `@page` dimensions are accurately synchronized. +> Both Word Twips and CSS `@page` dimensions are accurately synchronized.## Heading 2### Heading 3 +| Column 1 | Column 2 | Column 3 | +| :--- | :---: | ---: | +| Data A | Data B | Data C | +| Data D | Data E | Data F | + +| Column 1 | Column 2 | Column 3 | +| :--- | :---: | ---: | +| Data A | Data B | Data C | +| Data D | Data E | Data F | + +| Column 1 | Column 2 | Column 3 | +| :--- | :---: | ---: | +| Data A | Data B | Data C | +| Data D | Data E | Data F | + + +--- + +# Mathematical Foundations + +MarkForge provides native KaTeX math compilation for both inline physics equations such as $E = mc^2$ and complex display block proofs[^euler]: + +$$\oint_C \mathbf{B} \cdot d\mathbf{l} = \mu_0 \left( I_{\text{enc}} + \varepsilon_0 \frac{d\Phi_E}{dt} \right)$$ + +--- + +# Multi-Column Comparison + +:::columns 2 +:::col +### Cloud Architecture +- Globally distributed edge routing +- Low-latency WebSocket connections +- Automatic failover clustering +::: +:::col +### On-Premises Security +- Air-gapped deployment support +- Hardware Security Module (HSM) keys +- Strict local audit logging +::: +::: --- @@ -84,6 +126,8 @@ def check_environment(): | 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 | +| Native DOCX Packager | `full-all-properties.docx` | 45ms | Pixel-Perfect Word Layout | +| Headless Chromium | `full-all-properties.pdf` | 650ms | 300 DPI Vector PDF | +| HTML5 Bundler | `full-all-properties.html` | 18ms | Single Self-Contained File | + +[^euler]: Maxwell, J. C. (1865). A Dynamical Theory of the Electromagnetic Field. Philosophical Transactions of the Royal Society of London. diff --git a/apps/cli/markforge/examples/5-preview-server.ts b/apps/cli/markforge/examples/5-preview-server.ts new file mode 100644 index 0000000..252fff4 --- /dev/null +++ b/apps/cli/markforge/examples/5-preview-server.ts @@ -0,0 +1,29 @@ +/** + * Example 5: Interactive Live-Reload Preview Server + * Demonstrates starting the programmatic preview server with SSE auto-reload. + * + * Author: Ma'sum (@masumrpg) + */ +import * as path from "node:path"; +import { startPreviewServer } from "@masumdev/markforge"; + +async function main() { + const filePath = path.resolve(__dirname, "../docs/full-all-properties.md"); + const port = 3000; + + console.log("Starting MarkForge Live-Reload Preview Server..."); + const server = await startPreviewServer({ + filePath, + port, + open: true, // Automatically opens your default web browser + }); + + console.log(`Server is running at: ${server.url}`); + console.log(`Watching file: ${filePath}`); + console.log("Press Ctrl+C to terminate the server."); +} + +main().catch((err) => { + console.error("Failed to start preview server:", err); + process.exit(1); +}); diff --git a/apps/cli/markforge/examples/6-bulk-generation.ts b/apps/cli/markforge/examples/6-bulk-generation.ts new file mode 100644 index 0000000..2b0cc16 --- /dev/null +++ b/apps/cli/markforge/examples/6-bulk-generation.ts @@ -0,0 +1,170 @@ +/** + * Example 6: Bulk & Batch Document Generation + * Demonstrates compiling multiple documents in parallel from dynamic data and templates. + * + * Author: Ma'sum (@masumrpg) + */ +import * as path from "node:path"; +import * as fs from "node:fs"; +import { compileMarkdown, Theme, OutputFormat } from "@masumdev/markforge"; + +// Sample dataset: 5 enterprise quarterly department reports +const departmentReports = [ + { + dept: "Engineering", + lead: "Alexander Wright", + budget: "$1,450,000", + status: "On Track", + kpis: ["99.99% Uptime", "38 Releases Shipped", "0 Security Breaches"], + }, + { + dept: "Product Design", + lead: "Sarah Jenkins", + budget: "$620,000", + status: "Completed", + kpis: ["Design System 2.0", "14 Usability Labs", "NPS Score 78"], + }, + { + dept: "Infrastructure & Security", + lead: "David Chen", + budget: "$890,000", + status: "On Track", + kpis: ["Multi-Region Failover", "Zero-Trust mTLS", "SOC2 Compliance"], + }, + { + dept: "Finance & Operations", + lead: "Ma'sum", + budget: "$2,100,000", + status: "Reviewed", + kpis: ["18% Cost Optimization", "Clean Audit Report", "Automated Payroll"], + }, + { + dept: "Customer Success", + lead: "Elena Rostova", + budget: "$480,000", + status: "Exceeded", + kpis: ["< 15min SLA Response", "96% CSAT Score", "Zero Churn Key Accounts"], + }, +]; + +async function main() { + const outputDir = path.resolve(__dirname, "../.temp/bulk-reports"); + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + console.log(`Starting bulk compilation of ${departmentReports.length} department reports...`); + const startTime = Date.now(); + + // Parallel Batch Compilation using Promise.all + const results = await Promise.all( + departmentReports.map(async (data, index) => { + // 1. Construct dynamic Markdown template for each record + const markdown = `--- +title: "${data.dept} - Quarterly Performance Report" +subtitle: "Executive Summary & Operational Review — Q3 2026" +author: "${data.lead}" +company: "Enterprise Global Holdings" +version: "1.0.${index + 1}" +date: "${new Date().toISOString().split("T")[0]}" +theme: "corporate" +toc: true +coverPage: + enabled: true + preset: "modern" + badge: "${data.status.toUpperCase()}" + badgeColor: "#ECFDFD" + badgeTextColor: "#0D998D" + footerText: "Confidential Internal Report — Masum Dev Monorepo" +numberHeadings: + enabled: true + depth: 2 +--- + +# Executive Summary + +This document presents the Q3 operational review for the **${data.dept}** division. Under the leadership of **${data.lead}**, the department maintained an allocated operating budget of **${data.budget}**. + +> [!NOTE] +> Operational status for this period is verified as **${data.status}**. + +--- + +# Key Performance Indicators + +Below is the summary table of verified achievements for Q3: + +| Metric Indicator | Status | Quarter Goal | +| :--- | :---: | ---: | +${data.kpis.map((kpi, i) => `| KPI #${i + 1}: ${kpi} | Passed | 100% |`).join("\n")} + +--- + +# Strategic Architecture & Workflow + +\`\`\`mermaid +graph LR + A[Planning] --> B(${data.dept} Pipeline) + B --> C{Review} + C -->|Approved| D[Execution] + C -->|Revisions| B +\`\`\` + +--- + +:::columns 2 +:::col +### Department Lead +- Name: ${data.lead} +- Role: Division Principal +- Office: Global HQ +::: +:::col +### Operational Metrics +- Budget: ${data.budget} +- Fiscal Period: Q3 2026 +- Compliance: ISO 27001 +::: +::: + +--- + +# Signatures & Verification + +Academic and administrative sign-off verification[^audit]. + +[^audit]: Verified by independent internal audit procedures pursuant to enterprise bylaws. +`; + + const fileBaseName = `Report-${data.dept.toLowerCase().replace(/[^a-z0-9]+/g, "-")}.md`; + const tempMdPath = path.join(outputDir, fileBaseName); + fs.writeFileSync(tempMdPath, markdown, "utf-8"); + + // 2. Compile directly to DOCX, PDF, and HTML + const compileRes = await compileMarkdown(tempMdPath, { + to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], + outputDir, + theme: Theme.CORPORATE, + }); + + console.log(`[OK] Generated: ${data.dept} (${compileRes.files.length} files in ${compileRes.durationMs}ms)`); + return compileRes; + }) + ); + + const totalDuration = Date.now() - startTime; + const totalFiles = results.reduce((acc, r) => acc + r.files.length, 0); + + console.log("======================================================"); + console.log(`Bulk generation completed successfully!`); + console.log(`Total Documents : ${departmentReports.length}`); + console.log(`Total Files : ${totalFiles} files (.docx, .pdf, .html)`); + console.log(`Total Time : ${totalDuration}ms (average ${Math.round(totalDuration / departmentReports.length)}ms/doc)`); + console.log(`Output Folder : ${outputDir}`); + console.log("======================================================"); +} + +main().catch((err) => { + console.error("Bulk generation error:", err); + process.exit(1); +}); diff --git a/apps/cli/markforge/markforge.config.ts b/apps/cli/markforge/markforge.config.ts index c6f5e24..075dda5 100644 --- a/apps/cli/markforge/markforge.config.ts +++ b/apps/cli/markforge/markforge.config.ts @@ -20,4 +20,12 @@ export default defineConfig({ }, toc: true, embedImages: true, + + // Optional enterprise features (uncomment to activate): + // coverPage: { enabled: true, preset: "modern", logo: "./assets/company-logo.png", badge: "SPECIFICATION" }, + // backCover: { enabled: true, preset: "corporate", logo: "./assets/company-logo.png" }, + // numberHeadings: { enabled: true, depth: 3 }, + // watermark: "CONFIDENTIAL", + // security: { userPassword: "open_password", ownerPassword: "admin_password", permissions: { modifying: false, copying: false } }, + // math: true, }); diff --git a/apps/cli/markforge/markforge.full-config.ts b/apps/cli/markforge/markforge.full-config.ts index 434ae88..63ab842 100644 --- a/apps/cli/markforge/markforge.full-config.ts +++ b/apps/cli/markforge/markforge.full-config.ts @@ -11,24 +11,42 @@ import { /** * MarkForge Complete Specification Configuration Sample * Demonstrates 100% of all configurable properties in MarkforgeConfig with full Enum autocompletion. + * Any property that is optional or alternative is also presented as a commented line for quick toggling. */ export default defineConfig({ - // Target output formats (using OutputFormat enum or strings) + // Target output formats (using OutputFormat enum or strings: "docx" | "pdf" | "html" | "png") to: [OutputFormat.DOCX, OutputFormat.PDF, OutputFormat.HTML], + // to: [OutputFormat.PDF], // Single target compilation - // Output directory + // Output directory where compiled documents will be saved outputDir: ".temp/output-full-spec", - // Visual document theme (using Theme.CORPORATE preset or a custom ThemeProps object) + // Visual document theme (preset enum: CORPORATE, ACADEMIC, MODERN, MINIMAL, DRACULA, NORD, etc. or custom ThemeProps object) theme: Theme.CORPORATE, + // Custom theme palette example (uncomment to override completely): + // theme: { + // primaryColor: "#0D998D", + // primaryDark: "#0B7A70", + // primaryLight: "#ECFDFD", + // backgroundColor: "#FFFFFF", + // textColor: "#0F172A", + // textMuted: "#64748B", + // borderColor: "#E2E8F0", + // cardBackground: "#F8FAFC", + // codeBackground: "#1E293B", + // codeText: "#38BDF8", + // fontFamily: "Inter, -apple-system, sans-serif", + // fontMono: "Fira Code, monospace", + // }, - // Page orientation (using Orientation enum or strings) + // Page orientation (Orientation.PORTRAIT or Orientation.LANDSCAPE) orientation: Orientation.PORTRAIT, + // orientation: Orientation.LANDSCAPE, - // Standard paper size (using PaperSizeEnum or strings) + // Standard paper size (A4, Letter, Legal, A3, A5) paperSize: PaperSizeEnum.A4, - // Page margins with CSS unit or pt numbers + // Page margins (supports CSS units: "3cm", "25mm", "1in", or numbers in pt) margins: { top: "3cm", bottom: "2.5cm", @@ -88,6 +106,53 @@ export default defineConfig({ // Automatic Table of Contents generation toc: true, + // Dedicated Standalone Front Cover Page Builder + coverPage: { + enabled: true, + preset: "modern", // Options: "modern" | "corporate-split" | "minimal" | "card" + title: "{title}", + subtitle: "{subtitle}", + author: "{author}", + company: "{company}", + version: "v{version}", + date: "{date}", + badge: "CONFIDENTIAL SPECIFICATION", + badgeColor: "#ECFDFD", + badgeTextColor: "#0D998D", + logo: "./assets/company-logo.png", // Company logo path or Base64 data URI + logoWidth: 120, // Logo display width in px + footerText: "Proprietary & Confidential — Masum Dev Technologies", + }, + // coverPage: false, // Set to false to disable front cover page entirely + + // Hierarchical decimal section numbering (e.g. 1., 1.1, 1.1.1) + numberHeadings: { + enabled: true, + depth: 3, // Max heading depth to number (H1..H3) + skipH1: false, // Number from H1 downwards (set true if H1 is document title) + prefix: "", // Optional prefix (e.g. "Section ") + }, + // numberHeadings: false, // Set to false to disable section numbering + + // LaTeX Math Equation rendering ($inline$ and $$block$$) + math: true, + + // PDF Document Security & Password Encryption + security: { + userPassword: "masumdev_secret", // Password required to open document + ownerPassword: "masumdev_admin", // Master password required to modify permissions + permissions: { + printing: "highResolution", // "highResolution" | "lowResolution" | false + modifying: false, + copying: true, + annotating: true, + fillingForms: true, + contentAccessibility: true, + documentAssembly: false, + }, + }, + // security: undefined, // Omit or leave undefined for unrestricted standard PDF + // Watermark configuration (using WatermarkPosition enum or strings) watermark: { text: "CONFIDENTIAL DRAFT", @@ -95,11 +160,13 @@ export default defineConfig({ opacity: 0.1, fontSize: 52, rotate: -45, - position: WatermarkPosition.DIAGONAL, + position: WatermarkPosition.DIAGONAL, // "diagonal" | "center" | "top-right" | "bottom-right" }, + // watermark: false, // Set to false to disable watermark // Code block syntax highlighting theme (using SyntaxTheme enum or strings) syntaxTheme: SyntaxTheme.DRACULA, + // syntaxTheme: "github-dark", // "github-dark" | "github-light" | "dracula" | "monokai" | "nord" // Assets and bundling flags embedImages: true, @@ -107,14 +174,15 @@ export default defineConfig({ // Document signature & approval blocks (1 to 4 signatory slots with customizable style and alignment) signatures: { - align: "space-between", - style: "line", + align: "space-between", // "space-between" | "center" | "left" | "right" + style: "line", // "line" | "box" | "clean" items: [ { title: "Prepared by", name: "{author}", role: "Lead Platform Architect", date: "{date}", + // signatureImage: "./assets/signatures/author.png", // Optional signature image }, { title: "Reviewed by", @@ -130,6 +198,29 @@ export default defineConfig({ }, ], }, + // signatures: undefined, // Omit or set to undefined if no approval blocks required + + // Dedicated Standalone Back Cover / Closing Page + backCover: { + enabled: true, + preset: "corporate", // Options: "modern" | "corporate" | "minimal" | "contact-card" + title: "Thank You", + subtitle: "Masum Dev Technologies — Next-Gen Cross-Platform Engineering", + logo: "./assets/company-logo.png", + logoWidth: 120, + company: "{company}", + address: "Jakarta, Indonesia", + email: "contact@masumdev.com", + phone: "+62 812 3456 7890", + website: "https://react-native-library-docs.netlify.app", + social: { + github: "https://github.com/masumrpg", + // twitter: "https://x.com/masumdev", + // linkedin: "https://linkedin.com/in/masumdev", + }, + copyright: "Copyright (c) {year} {company}. All Rights Reserved.", + }, + // backCover: false, // Set to false to disable back cover closing page // Fallback metadata dictionary metadata: { @@ -141,4 +232,13 @@ export default defineConfig({ date: "2026-08-29", lang: "en", }, + + // Custom external stylesheet path(s) to inject into HTML and PDF documents + // css: ["./custom-styles.css"], + + // Live preview & watch development options + watch: false, // Set to true to automatically re-compile when markdown files change + serve: false, // Set to true to start the interactive Web Studio Preview server + port: 4000, // Custom port for live studio (default: 4000) + open: false, // Set to true to automatically launch the default browser }); diff --git a/apps/cli/markforge/package.json b/apps/cli/markforge/package.json index ca42c1d..e3cca6b 100644 --- a/apps/cli/markforge/package.json +++ b/apps/cli/markforge/package.json @@ -14,11 +14,15 @@ "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", "watch": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md --watch -o .temp/output", + "serve": "bun ../../../packages/markforge/dist/cli.mjs ./docs/sample.md --serve 3000 --open", + "serve:spec": "bun ../../../packages/markforge/dist/cli.mjs ./docs/full-all-properties.md -c ./markforge.full-config.ts --serve 3000 --open", "example:minimal": "bun ./examples/1-minimal.ts", "example:intermediate": "bun ./examples/2-intermediate.ts", "example:full": "bun ./examples/3-full-custom.ts", "example:ast": "bun ./examples/4-low-level-ast.ts", - "example:all": "bun run example:minimal && bun run example:intermediate && bun run example:full && bun run example:ast" + "example:serve": "bun ./examples/5-preview-server.ts", + "example:bulk": "bun ./examples/6-bulk-generation.ts", + "example:all": "bun run example:minimal && bun run example:intermediate && bun run example:full && bun run example:ast && bun run example:bulk" }, "dependencies": { "@masumdev/markforge": "workspace:*" diff --git a/apps/docs/src/content/docs/markforge/changelog.mdx b/apps/docs/src/content/docs/markforge/changelog.mdx index 97e5bc0..3d2b49d 100644 --- a/apps/docs/src/content/docs/markforge/changelog.mdx +++ b/apps/docs/src/content/docs/markforge/changelog.mdx @@ -6,14 +6,53 @@ sidebar: description: Release history and version notes for @masumdev/markforge. --- +## [0.4.0] - 2026-08-29 + +### Major Enterprise Features & Security Release + +- **ISO 32000-2 AES-256 PDF Security & Password Encryption (`security`)**: + - Native standard PDF encryption with independent `userPassword` (password required to open the document) and `ownerPassword` (master password required to modify document permissions). + - Granular permissions enforcement: `printing`, `modifying`, `copying`, `annotating`, `fillingForms`, `contentAccessibility`, and `documentAssembly`. + - Supports permission-only protection mode when `userPassword` is left empty. +- **100% Unselectable Image XObject Watermark**: + - Replaced live text glyphs with transparent Chromium-rendered PNG buffers embedded via `pdfDoc.drawImage()` as PDF Image XObjects. + - Eliminates 100% of selectable text stream tokens (`Tj`/`TJ`), preventing any cursor interference or unwanted text highlights during document review. + - Automatically centered diagonally (`rotate: -45`) across content pages and excluded from Front Cover and Back Cover. +- **Cover Page Builder with Company Logo Embedding (`coverPage`)**: + - Four enterprise design presets: `modern`, `corporate-split`, `minimal`, and `card`. + - Native image embedding for brand logos across DOCX, PDF, and HTML with configurable `logoWidth`. + - Generates dedicated unnumbered first sections in Microsoft Word DOCX and suppresses running headers/footers via CSS `@page :first` in PDF. +- **Back Cover / Closing Page Builder (`backCover`)**: + - Four closing design presets: `modern`, `corporate`, `minimal`, and `contact-card`. + - Full-bleed edge-to-edge dark closing card in PDF and isolated final section in DOCX. + - Company profile grid including address, email, phone, website, and social links. +- **Math & LaTeX Equation Rendering (`math`)**: + - Native KaTeX in-memory math parsing for inline formulas (`$E=mc^2$`) and display block equations (`$$\int e^x dx$$`). + - Self-contained inlined KaTeX CSS for zero-network HTML and PDF generation. + - Cambria Math typography and shaded equation containers in Microsoft Word DOCX. +- **Multi-Column Container Directives (`:::columns`, `:::col`)**: + - Multi-column container syntax with configurable columns count and gap (`:::columns 2` or `:::columns [cols=3 gap=2rem]`). + - Emitted as CSS Grid layouts in HTML/PDF and native borderless multi-cell table structures in DOCX. +- **Hierarchical Section Numbering (`numberHeadings`)**: + - Automated decimal heading prefixes (`1.`, `1.1.`, `1.1.1.`) with configurable depth and optional H1 skip. + - Synchronized Table of Contents entries and anchor slugs. +- **Footnotes & Endnotes (`[^1]`, `[^1]: text`)**: + - Markdown footnote references (`[^id]`) and bottom definition parsing. + - Formatted as superscripts with return links in HTML/PDF and dedicated numbered sections in Word DOCX. +- **Windows Enterprise & Strict Corporate Policy Compliance**: + - Prioritizes Microsoft Edge (`msedge.exe`) with AppLocker and GPO whitelist compatibility. + - Isolated process arguments without suspicious flags (`--disable-sync`, isolated temp profile). + - Added `windowsHide: true` on child processes to eliminate console prompt flashes on Windows. +- **Interactive Live-Reload Preview Server (`markforge --serve`)**: + - Real-time Server-Sent Events (SSE) `/events` broadcast on file or config changes. + - Automatic scroll position and reading preservation across live reloads. + +--- + ## [0.3.0] - 2026-08-29 ### Features & Enhancements -- **100% Unselectable Vector Watermarks in PDF**: - - Replaced SVG text nodes with dynamic high-DPI HTML5 Canvas 2D rasterization (`canvas.toDataURL('image/png')` at 3x DPR). - - Emitted as a pure PNG Image XObject background layer in Chromium PDF, completely eliminating selectable text layer glyphs. - - Ensures clean cursor dragging and text selection across all PDF reader applications. - **Comprehensive Programmatic API Suite**: - Added full programmatic compilation examples in `apps/cli/markforge/examples/`: - `1-minimal.ts`: Zero-configuration Markdown-to-MultiFormat execution. @@ -25,7 +64,7 @@ description: Release history and version notes for @masumdev/markforge. - Added author attribution (`Author: Ma'sum (@masumrpg)`) and direct repository links. - Removed all emojis across CLI terminal outputs for maximum shell compatibility. - **Author Identity & Workspace Normalization**: - - Replaced all legacy author references with `Ma'sum` and updated GitHub profiles to `https://github.com/masumrpg`. + - Normalized author identity to `Ma'sum` and updated GitHub profiles to `https://github.com/masumrpg`. --- @@ -34,126 +73,45 @@ description: Release history and version notes for @masumdev/markforge. ### Features & Enhancements - **Document Signatures & Approval Blocks (`signatures`)**: - - Add formal approval and sign-off blocks (*Lembar Pengesahan / Tanda Tangan*) with 1 to 4 signatory slots. + - Add formal approval and sign-off blocks with 1 to 4 signatory slots. - Multi-alignment support (`left`, `center`, `right`, `space-between`). - Multiple visual styles: `line` (classic underline divider), `box` (bordered card), and `clean` (borderless). - - Dynamic token interpolation (`{author}`, `{company}`, `{date}`, etc.) and optional digital signature image/seal embedding. - - Consistent and responsive rendering across **DOCX (Word)**, **Chromium PDF**, and **HTML**. -- **Native Word Paragraph TabStops for Running Headers & Footers**: Replaced 3-column table headers and footers with native Microsoft Word OpenXML `TabStopType.CENTER` and `TabStopType.RIGHT` paragraph architecture. Eliminates 100% of table gridline boundaries/box outlines in LibreOffice Writer, Google Docs, and Microsoft Word when dividers are disabled. -- **Synchronized Header & Footer Divider Lines**: Solid horizontal separator lines render cleanly via `border.bottom` / `border.top` on Word paragraphs and fixed print dividers (`@media print`) on Chromium PDF. + - Dynamic token interpolation (`{author}`, `{company}`, `{date}`) and optional digital signature image/seal embedding. +- **Native Word Paragraph TabStops for Running Headers & Footers**: + - Replaced 3-column table headers and footers with native Microsoft Word OpenXML `TabStopType.CENTER` and `TabStopType.RIGHT` paragraph architecture. + - Eliminates 100% of table gridline boundaries in LibreOffice Writer, Google Docs, and Microsoft Word when dividers are disabled. +- **Synchronized Header & Footer Divider Lines**: + - Solid horizontal separator lines render cleanly via `border.bottom` / `border.top` on Word paragraphs and fixed print dividers (`@media print`) on Chromium PDF. - **DOCX Visual & Typography Overhaul**: - - **Cover & Title Area**: 22pt bold title, 12pt subtitle, and metadata row with bottom border. - - **Table of Contents (TOC) Card**: Auto-generated shaded card box (`#F8FAFC`, border `#E2E8F0`, header `TABLE OF CONTENTS`, indented H1-H3 in `primaryDark`). - - **Headings**: H1 with cyan solid bottom border (`#33CDCF`), H2 with subtle bottom border (`#E2E8F0`), and H3. - - **Paragraphs & Lists**: Charcoal `#334155` text with comfortable line spacing (`line: 280` / `260`). - - **Callout Alert Boxes**: 20pt left border in alert color, tinted background fill (`#ECFDFD`, `#ECFDF5`, etc.), bold badge `[NOTE]`, and padded body text. - - **Zebra Tables**: Shaded header `#F1F5F9` with `#CBD5E1` border and alternating rows (`#FFFFFF` and `#F8FAFC`). - - **Code Blocks**: Formatted table card with `#33CDCF` left accent border, `#F8FAFC` background, monospace `Consolas` text, and syntax token colors. + - Enhanced title area with 22pt bold title, 12pt subtitle, and bottom border. + - Auto-generated Table of Contents card with shaded container (`#F8FAFC`, border `#E2E8F0`). + - Shaded zebra table rows with alternating fills. --- ## [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 - -- **Node.js 22/25 `--localstorage-file` warning**: Eliminated the webstorage warning caused by `docx` checking `globalThis.localStorage` by using dynamic imports in the CLI entry point. -- **CLI Compilation Error**: Resolved the `expected input to be a string or buffer` error caused by an `inputPath` vs `inputFile` prop mismatch in ``. -- **Global Package Versioning**: Fixed version resolution in ESM bundles to correctly detect `0.2.3` across global and local installations. - ---- - -## [0.2.2] - 2026-08-28 - -### 🛠️ Fixed - -- **CLI `--localstorage-file` warning still appearing (regression from 0.2.1)**: The warning suppression logic had an inverted listener order bug: - ```typescript - // ❌ Wrong — handler added then immediately removed - process.on("warning", handler); - process.removeAllListeners("warning"); // removes our handler too! - - // ✅ Fixed — remove defaults first, then add our filter - process.removeAllListeners("warning"); - process.on("warning", handler); - ``` -- **Dynamic `markforge -V` version**: `MARKFORGE_VERSION` now reads directly from `package.json` at runtime instead of a hardcoded string. Future version bumps only require updating `package.json`. - ---- - -## [0.2.1] - 2026-08-28 - -### 🛠️ Fixed - -- **Windows PDF not using Chromium (fallback PDF rendered)**: `findChromeExecutable()` now resolves Windows browser paths using **runtime environment variables** (`process.env.LOCALAPPDATA`, `process.env.PROGRAMFILES`) instead of hardcoded strings. This fixes the most common Windows failure case where Chrome is installed at the user level (`%LOCALAPPDATA%\Google\Chrome\...`) rather than the system level. - - Full detection order on Windows: - | Browser | Path Source | - | :--- | :--- | - | Chrome (user) | `%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe` | - | Chrome (system) | `%PROGRAMFILES%\Google\Chrome\Application\chrome.exe` | - | **Edge** (user) | `%LOCALAPPDATA%\Microsoft\Edge\Application\msedge.exe` | - | **Edge** (system) | `%PROGRAMFILES%\Microsoft\Edge\Application\msedge.exe` | - | Chromium | `%LOCALAPPDATA%\Chromium\Application\chrome.exe` | - | PATH search | `where chrome`, `where msedge` | - - **Override**: Set `CHROME_PATH` or `PUPPETEER_EXECUTABLE_PATH` environment variable to use a custom browser binary. - -- **CLI `--localstorage-file` warning**: Suppressed the noisy Bun startup warning `--localstorage-file was provided without a valid path` that appeared on every `markforge` invocation. Bun's built-in localStorage API emits this warning when no storage file is configured. MarkForge never uses localStorage, so the warning is filtered silently at process startup. - -- **Mermaid renderer reliability**: Added `--virtual-time-budget=5000` and `--disable-software-rasterizer` to the Chromium invocation for Mermaid diagram capture. Improves render success rate on Windows and lower-end hardware where GPU rasterization is unavailable. - ---- - -## [0.2.0] - 2026-08-27 - -### 🚀 Added - -- **Mermaid Diagram Support**: Full Mermaid.js rendering for all output formats. Diagrams in ` ```mermaid ``` ` blocks are rasterized via headless Chromium and embedded as images in DOCX, PDF, and HTML. Virtual-time-budget set to 8000ms for complex diagrams. -- **Callout / Alert Boxes**: GitHub-flavored callout syntax (`> [!NOTE]`, `> [!TIP]`, `> [!WARNING]`, `> [!CAUTION]`, `> [!IMPORTANT]`) now renders with colored left-border and background in all three output formats. Colors use inline `style` attributes to bypass Chromium print background suppression. -- **Watermark Configuration**: Optional per-document watermark support via `markforge.config.ts` or frontmatter. Disabled by default. Configurable `text`, `opacity`, and `position` (`diagonal`, `center`, `top-left`, `top-right`, `bottom-left`, `bottom-right`). -- **Multi-Zone Header & Footer**: Full Left / Center / Right header and footer support in both DOCX (OpenXML tab-stop logic) and PDF (CSS Paged Media `@top-*` / `@bottom-*` rules). Supports `{title}`, `{author}`, `{version}`, `{date}`, `{page}`, `{pages}` tokens. -- **`THEME_COMPONENTS` Base Layer**: All shared component CSS (callouts, blockquotes, code blocks, tables, images, TOC, document header, print rules) extracted into a `THEME_COMPONENTS` constant always injected regardless of the chosen theme. Fixes missing styles in `academic` and all non-default themes. - -### 🎨 Changed - -- **Primary Color Palette**: Updated to Blu by BCA Digital cyan — primary `#33CDCF`, deep `#009DA0`, tint `#ECFDFD` — across all themes and component styles. -- **Dual Syntax Highlight Themes**: - - **PDF / HTML** (dark background): VS Code Dark+-inspired palette. Strings in warm orange `#E07C4F`, keywords in coral-red `#FF7B72`, plain text in light gray `#E2E8F0`, functions in soft purple `#D2A8FF`. - - **DOCX** (white background): GitHub Light-inspired palette. Strings in forest teal `#0A7E5C`, keywords in crimson `#D73A49`, plain text near-black `#24292E`. - - `tokenizeCodeLine(line, lang, theme)` now accepts a `theme: "dark" | "light"` parameter. `docxBuilder` passes `"light"`, HTML/PDF builders use `"dark"` (default). -- **PDF Code Block Scrollbar Removed**: `@media print { pre { overflow: visible; white-space: pre-wrap; word-break: break-all; } }` — scrollbars no longer appear in printed/PDF code blocks. -- **`THEME_ACADEMIC`** now includes a full `:root` CSS variable block compatible with `THEME_COMPONENTS`, enabling all component styles (callouts, code, tables) to apply correctly in academic theme documents. - -### 🛠️ Fixed +### Features & Enhancements -- **PDF Header/Footer not rendering**: Refactored `injectPagedMediaStyles()` in `pdfBuilder.ts` to correctly inject CSS `@page` rules using `@top-left`, `@top-center`, `@top-right`, `@bottom-*` selectors from frontmatter/config. -- **DOCX Header/Footer single-zone bug**: Implemented `TabStopType` logic to correctly split headers into three zones using tab characters and `TabStop` positions in OpenXML paragraphs. -- **Callout styles missing in PDF**: Root cause was `THEME_ACADEMIC` missing component CSS. Fixed by the `THEME_COMPONENTS` extraction + injection approach in `htmlBuilder.ts`. -- **Chromium PDF background color suppression**: Added `--force-color-profile=srgb` Chrome flag. Callout colors rendered using inline styles that bypass print background graphics suppression. -- **Mermaid diagram alignment**: Fixed `virtual-time-budget` increased to `8000ms` to ensure complex diagrams finish rendering before PDF capture. +- **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. --- -## [0.1.0] - 2026-08-27 +## [0.1.0] - 2026-08-28 -### 🚀 Initial Release +### Initial Release -- **Multi-Format Document Engine**: Full compiler pipeline producing Microsoft Word (`.docx`), Print-ready (`.pdf`), and Standalone (`.html`). -- **Embedded HTML & CSS Support**: Native parsing of inline HTML tags (`
`, ``, ``, ``, ` -${watermarkHtml}
-${bodyHtml}${signaturesHtml}
+${watermarkHtml}${coverHtml}
+${bodyHtml}${footnotesHtml}${signaturesHtml}
${mermaidScript} - +${backHtml}`; } diff --git a/packages/markforge/src/core/imageResolver.ts b/packages/markforge/src/core/imageResolver.ts index 2ba9893..c8addd2 100644 --- a/packages/markforge/src/core/imageResolver.ts +++ b/packages/markforge/src/core/imageResolver.ts @@ -86,9 +86,14 @@ export async function resolveImage( } // 3. Local File Path - const localPath = path.isAbsolute(src) ? src : path.resolve(baseDir, src); + let localPath = path.isAbsolute(src) ? src : path.resolve(baseDir, src); if (!fs.existsSync(localPath)) { - return null; + const cwdPath = path.resolve(process.cwd(), src); + if (fs.existsSync(cwdPath)) { + localPath = cwdPath; + } else { + return null; + } } const buffer = fs.readFileSync(localPath); diff --git a/packages/markforge/src/core/math/mathRenderer.ts b/packages/markforge/src/core/math/mathRenderer.ts new file mode 100644 index 0000000..13d1e5e --- /dev/null +++ b/packages/markforge/src/core/math/mathRenderer.ts @@ -0,0 +1,52 @@ +import katex from "katex"; + +export interface MathRenderResult { + html: string; + isBlock: boolean; + rawLatex: string; +} + +/** + * Renders a LaTeX math formula into clean HTML/MathML markup using KaTeX. + */ +export function renderMathToHtml(latex: string, displayMode: boolean = false): string { + try { + return katex.renderToString(latex.trim(), { + displayMode, + throwOnError: false, + output: "htmlAndMathml", + strict: false, + }); + } catch { + // Fallback if KaTeX encounters parsing error + return `${latex}`; + } +} + +/** + * Minimal inlined KaTeX CSS required for rendering formulas without external network requests. + */ +export const KATEX_INLINE_CSS = ` +.katex { font: normal 1.21em KaTeX_Main, Times New Roman, serif; line-height: 1.2; text-indent: 0; text-rendering: auto; border-color: currentColor; } +.katex * { -ms-high-contrast-adjust: none !important; } +.katex .katex-html { display: inline-block; } +.katex .katex-mathml { clip: rect(1px, 1px, 1px, 1px); border: 0; height: 1px; overflow: hidden; padding: 0; position: absolute; width: 1px; } +.katex-display { display: block; margin: 1em 0; text-align: center; } +.katex-display > .katex { display: inline-block; text-align: initial; } +.katex .base { position: relative; white-space: nowrap; width: min-content; } +.katex .strut { display: inline-block; } +.katex .mord { display: inline-block; } +.katex .mbin { display: inline-block; } +.katex .mrel { display: inline-block; } +.katex .mopen { display: inline-block; } +.katex .mclose { display: inline-block; } +.katex .mpunct { display: inline-block; } +.katex .minner { display: inline-block; } +.katex .mop { display: inline-block; } +.katex .frac-line { width: 100%; border-bottom-style: solid; } +.katex .vlist-t { display: inline-table; table-layout: fixed; } +.katex .vlist-r { display: table-row; } +.katex .vlist { display: table-cell; vertical-align: bottom; position: relative; } +.katex .msupsub { text-align: left; } +.katex .sqrt > .root { margin-left: 0.27777778em; margin-right: -0.55555556em; } +`; diff --git a/packages/markforge/src/core/parser.ts b/packages/markforge/src/core/parser.ts index a33bd0f..e5c18fd 100644 --- a/packages/markforge/src/core/parser.ts +++ b/packages/markforge/src/core/parser.ts @@ -1,5 +1,5 @@ import matter from "gray-matter"; -import type { FrontmatterMetadata } from "../config/types.js"; +import type { FrontmatterMetadata, NumberHeadingsOptions } from "../config/types.js"; export type MarkdownNodeType = | "heading" @@ -13,13 +13,27 @@ export type MarkdownNodeType = | "tableCell" | "codeBlock" | "mermaid" + | "mathBlock" + | "footnoteDef" + | "columns" + | "column" | "htmlBlock" | "thematicBreak" | "image" | "toc"; export interface MarkdownInlineSpan { - type: "text" | "bold" | "italic" | "code" | "link" | "strikethrough" | "image" | "htmlInline"; + type: + | "text" + | "bold" + | "italic" + | "code" + | "link" + | "strikethrough" + | "image" + | "htmlInline" + | "mathInline" + | "footnoteRef"; content: string; url?: string; title?: string; @@ -28,6 +42,7 @@ export interface MarkdownInlineSpan { height?: number | string; children?: MarkdownInlineSpan[]; style?: Record; + footnoteId?: string; } export interface MarkdownASTNode { @@ -45,6 +60,15 @@ export interface MarkdownASTNode { rawHtml?: string; id?: string; style?: Record; + columnsCount?: number; + columnGap?: string; + footnoteId?: string; +} + +export interface FootnoteDefinition { + id: string; + text: string; + inlines: MarkdownInlineSpan[]; } export interface ParsedMarkdownDocument { @@ -53,10 +77,11 @@ export interface ParsedMarkdownDocument { nodes: MarkdownASTNode[]; tocEntries: { id: string; text: string; level: number }[]; inlinedStyles: string[]; + footnoteDefs: FootnoteDefinition[]; } /** - * Parses inline formatting (bold, italic, code, links, images, HTML spans) + * Parses inline formatting (bold, italic, code, links, images, math, footnotes, HTML spans) */ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { const spans: MarkdownInlineSpan[] = []; @@ -93,7 +118,31 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 2. Link: [text](url "title") + // 2. Footnote reference: [^1] or [^note-id] + const fnMatch = remaining.match(/^\[\^([\w-]+)\]/); + if (fnMatch) { + const fnId = fnMatch[1]; + spans.push({ + type: "footnoteRef", + content: fnId, + footnoteId: fnId, + }); + remaining = remaining.slice(fnMatch[0].length); + continue; + } + + // 3. Inline Math: $math$ (e.g. $E=mc^2$) - must have non-space boundary and not $$ + const mathMatch = remaining.match(/^\$([^$\n]+?)\$/); + if (mathMatch && !mathMatch[1].startsWith("$")) { + spans.push({ + type: "mathInline", + content: mathMatch[1], + }); + remaining = remaining.slice(mathMatch[0].length); + continue; + } + + // 4. Link: [text](url "title") const linkMatch = remaining.match(/^\[([^\]]+)\]\(([^)\s]+)(?:\s+"([^"]+)")?\)/); if (linkMatch) { spans.push({ @@ -107,7 +156,7 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 3. Bold + Italic: ***text*** or ___text___ + // 5. Bold + Italic: ***text*** or ___text___ const boldItalicMatch = remaining.match(/^(\*\*\*|___)(.+?)\1/); if (boldItalicMatch) { spans.push({ @@ -125,7 +174,7 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 4. Bold: **text** or __text__ + // 6. Bold: **text** or __text__ const boldMatch = remaining.match(/^(\*\*|__)(.+?)\1/); if (boldMatch) { spans.push({ @@ -137,7 +186,7 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 5. Italic: *text* or _text_ + // 7. Italic: *text* or _text_ const italicMatch = remaining.match(/^(\*|_)([^*_]+?)\1/); if (italicMatch) { spans.push({ @@ -149,7 +198,7 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 6. Strikethrough: ~~text~~ + // 8. Strikethrough: ~~text~~ const strikeMatch = remaining.match(/^~~(.+?)~~/); if (strikeMatch) { spans.push({ @@ -161,7 +210,7 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 7. Inline code: `code` + // 9. Inline code: `code` const codeMatch = remaining.match(/^`([^`]+)`/); if (codeMatch) { spans.push({ @@ -172,7 +221,7 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 8. Raw HTML Tag: ... or or
+ // 10. Raw HTML Tag: ... or or
const htmlTagMatch = remaining.match(/^<(\w+)([^>]*)>(.*?)<\/\1>/i) || remaining.match(/^<(\w+)([^>]*)\/?>/i); if (htmlTagMatch) { const fullTag = htmlTagMatch[0]; @@ -209,8 +258,8 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { continue; } - // 9. Plain text up to next special character - const nextSpecial = remaining.search(/[\*\_\[\!`~<]/); + // 11. Plain text up to next special character + const nextSpecial = remaining.search(/[\*\_\[\!`~<\$]/); if (nextSpecial === -1) { spans.push({ type: "text", @@ -218,7 +267,6 @@ export function parseInlineSpans(text: string): MarkdownInlineSpan[] { }); break; } else if (nextSpecial === 0) { - // Escaped or single special char spans.push({ type: "text", content: remaining[0], @@ -247,6 +295,52 @@ export function slugify(text: string): string { .replace(/^-+|-+$/g, ""); } +/** + * Applies hierarchical numbering to headings (e.g. "1.", "1.1", "1.1.1"). + */ +function applyHeadingNumbering( + nodes: MarkdownASTNode[], + tocEntries: { id: string; text: string; level: number }[], + options: NumberHeadingsOptions +): void { + const depth = options.depth ?? 3; + const skipH1 = options.skipH1 ?? false; + const prefix = options.prefix ?? ""; + + const counters = [0, 0, 0, 0, 0, 0]; // H1..H6 counters + + for (const node of nodes) { + if (node.type === "heading" && node.level) { + const lvl = node.level; + if (lvl > depth) continue; + if (lvl === 1 && skipH1) continue; + + // Increment current level, reset child levels + const idx = lvl - 1; + counters[idx]++; + for (let c = idx + 1; c < counters.length; c++) { + counters[c] = 0; + } + + // Build numbering string (e.g. "1.2.3") + const startIdx = skipH1 ? 1 : 0; + const parts = counters.slice(startIdx, idx + 1).filter((n) => n > 0); + const numberStr = parts.join(".") + "."; + const fullPrefix = prefix ? `${prefix} ${numberStr} ` : `${numberStr} `; + + const originalText = node.text || ""; + node.text = fullPrefix + originalText; + node.inlines = parseInlineSpans(node.text); + + // Update matching TOC entry + const toc = tocEntries.find((t) => t.id === node.id); + if (toc) { + toc.text = node.text; + } + } + } +} + /** * Parses raw markdown string into rich structured AST nodes. */ @@ -264,6 +358,7 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume const lines = cleanContent.split(/\r?\n/); const nodes: MarkdownASTNode[] = []; const tocEntries: { id: string; text: string; level: number }[] = []; + const footnoteDefs: { id: string; text: string; inlines: MarkdownInlineSpan[] }[] = []; let i = 0; while (i < lines.length) { @@ -301,7 +396,34 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume continue; } - // 4. Code block (```lang ... ```) + // 4. Standalone Math Block ($$ ... $$) + if (line.trim().startsWith("$$")) { + const mathLines: string[] = []; + const singleLine = line.trim().match(/^\$\$(.+)\$\$$/); + if (singleLine) { + nodes.push({ + type: "mathBlock", + text: singleLine[1].trim(), + }); + i++; + continue; + } + + i++; // consume opening $$ + while (i < lines.length && !lines[i].trim().startsWith("$$")) { + mathLines.push(lines[i]); + i++; + } + if (i < lines.length) i++; // consume closing $$ + + nodes.push({ + type: "mathBlock", + text: mathLines.join("\n").trim(), + }); + continue; + } + + // 5. Code block (```lang ... ```) const codeBlockMatch = line.match(/^```(\w+)?/); if (codeBlockMatch) { const language = (codeBlockMatch[1] || "text").trim().toLowerCase(); @@ -330,7 +452,112 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume continue; } - // 5. Alert / Callout Block (> [!NOTE], > [!TIP], > [!WARNING], > [!IMPORTANT], > [!CAUTION]) + // 6. Multi-Column Container Directives (:::columns [cols=2 gap=1.5rem] ... :::) + const colsMatch = line.trim().match(/^:::columns(?:\s+\[?([\w\s=.-]+)\]?)?$/i); + if (colsMatch) { + const attrStr = colsMatch[1] || ""; + let colsCount = 2; + let colGap = "1.5rem"; + + if (attrStr) { + const numMatch = attrStr.trim().match(/^(\d+)$/); + const cMatch = attrStr.match(/cols=(\d+)/i) || attrStr.match(/columns=(\d+)/i); + const gMatch = attrStr.match(/gap=([^\s]+)/i); + if (numMatch) colsCount = parseInt(numMatch[1], 10); + else if (cMatch) colsCount = parseInt(cMatch[1], 10); + if (gMatch) colGap = gMatch[1]; + } + + const columnNodes: MarkdownASTNode[] = []; + let currentColumnLines: string[] = []; + let inColBlock = false; + i++; + + while (i < lines.length) { + const curLine = lines[i]; + const trimmed = curLine.trim(); + + if (/^:::col(?:umn)?$/i.test(trimmed)) { + if (currentColumnLines.length > 0) { + const subDoc = parseMarkdownDocument(currentColumnLines.join("\n")); + columnNodes.push({ + type: "column", + children: subDoc.nodes, + }); + currentColumnLines = []; + } + inColBlock = true; + i++; + } else if (trimmed === ":::") { + if (inColBlock) { + if (currentColumnLines.length > 0) { + const subDoc = parseMarkdownDocument(currentColumnLines.join("\n")); + columnNodes.push({ + type: "column", + children: subDoc.nodes, + }); + currentColumnLines = []; + } + inColBlock = false; + i++; + } else { + if (currentColumnLines.length > 0) { + const subDoc = parseMarkdownDocument(currentColumnLines.join("\n")); + columnNodes.push({ + type: "column", + children: subDoc.nodes, + }); + currentColumnLines = []; + } + i++; + break; + } + } else { + currentColumnLines.push(curLine); + i++; + } + } + + if (currentColumnLines.length > 0) { + const subDoc = parseMarkdownDocument(currentColumnLines.join("\n")); + columnNodes.push({ + type: "column", + children: subDoc.nodes, + }); + } + + nodes.push({ + type: "columns", + columnsCount: columnNodes.length > 0 ? columnNodes.length : colsCount, + columnGap: colGap, + children: columnNodes, + }); + continue; + } + + // 7. Footnote Definition Block ([^id]: Note explanation...) + const fnDefMatch = line.match(/^\[\^([\w-]+)\]:\s+(.+)$/); + if (fnDefMatch) { + const fnId = fnDefMatch[1]; + const fnText = fnDefMatch[2].trim(); + const inlines = parseInlineSpans(fnText); + + footnoteDefs.push({ + id: fnId, + text: fnText, + inlines, + }); + nodes.push({ + type: "footnoteDef", + footnoteId: fnId, + text: fnText, + inlines, + }); + i++; + continue; + } + + // 8. Alert / Callout Block (> [!NOTE], > [!TIP], > [!WARNING], > [!IMPORTANT], > [!CAUTION]) const calloutMatch = line.match(/^>\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*$/i); if (calloutMatch) { const calloutType = calloutMatch[1].toUpperCase() as MarkdownASTNode["calloutType"]; @@ -350,7 +577,7 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume continue; } - // 6. Blockquote (> quote) + // 9. Blockquote (> quote) if (line.startsWith(">")) { const quoteLines: string[] = []; while (i < lines.length && lines[i].startsWith(">")) { @@ -366,7 +593,7 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume continue; } - // 7. GFM Tables (| Header | Header |) + // 10. GFM Tables (| Header | Header |) if (line.trim().startsWith("|") && line.includes("|")) { const tableLines: string[] = []; while (i < lines.length && lines[i].trim().startsWith("|")) { @@ -432,7 +659,7 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume } } - // 8. Lists (Unordered & Ordered & Tasks) + // 11. Lists (Unordered & Ordered & Tasks) const listMatch = line.match(/^(\s*)([-*+]|\d+\.)\s+(.+)$/); if (listMatch) { const listItems: MarkdownASTNode[] = []; @@ -469,7 +696,7 @@ export function parseMarkdownDocument(rawMarkdown: string): ParsedMarkdownDocume continue; } - // 9. Raw HTML Block (e.g.
...
or
) + // 12. Raw HTML Block (e.g.
...
or
) if (line.trim().startsWith("<") && !line.trim().startsWith(" +
+ + + +
+ + + + +
+ + + +
+ + + + + +
+ + +
+
+ + + +
+ Saved + + + + +
+ + +
+ +
+
+ MARKDOWN SOURCE + Lines: 1 | Words: 0 | UTF-8 +
+
+
1
+ +
+
+ + +
+ + +
+
+ RENDERED PREVIEW +
+ + + +
+ Created by Ma'sum (@masumrpg) +
+
+ +
+
+
+ + + +`; + + res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); + res.end(appHtml); + return; + } + + res.writeHead(404, { "Content-Type": "text/plain" }); + res.end("Not Found"); + }); + + return new Promise((resolve, reject) => { + server.listen(port, () => { + const url = `http://localhost:${port}`; + resolve({ + server, + port, + url, + close: async () => { + watcher.close(); + sseClients.forEach((client) => { + try { + client.end(); + } catch { + // ignore + } + }); + sseClients.clear(); + return new Promise((res) => { + server.close(() => res()); + }); + }, + }); + }); + + server.on("error", (err) => { + reject(err); + }); + }); +} + +function escapeHtml(str: string): string { + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); +} diff --git a/packages/markforge/src/version.ts b/packages/markforge/src/version.ts index 9511b08..3b91d71 100644 --- a/packages/markforge/src/version.ts +++ b/packages/markforge/src/version.ts @@ -26,7 +26,7 @@ try { } /** Hardcoded fallback — only used if package.json cannot be found at runtime. */ -const FALLBACK_VERSION = "0.3.0"; +const FALLBACK_VERSION = "0.4.0"; /** * Walks up the directory tree from `fromDir` to find the nearest diff --git a/packages/markforge/test/enterpriseFeatures.test.ts b/packages/markforge/test/enterpriseFeatures.test.ts new file mode 100644 index 0000000..28338bb --- /dev/null +++ b/packages/markforge/test/enterpriseFeatures.test.ts @@ -0,0 +1,307 @@ +import { describe, it, expect } from "bun:test"; +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as os from "node:os"; +import { + parseMarkdown, + buildHtmlDocument, + buildDocxDocument, + buildPdfDocument, + startPreviewServer, + renderMathToHtml, + resolveDocumentConfig, + normalizeCoverPage, + normalizeBackCover, + normalizeNumberHeadings, + normalizeSecurity, +} from "../src/index.js"; + +describe("MarkForge Enterprise Features", () => { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "markforge_test_")); + + it("Feature 1: Cover Page Builder (Resolution & HTML/DOCX)", async () => { + const directCover = normalizeCoverPage({ enabled: true, preset: "card", title: "Direct Title" }); + expect(directCover?.preset).toBe("card"); + expect(directCover?.title).toBe("Direct Title"); + + const md = `--- +title: "Quarterly Financial Report" +subtitle: "Q3 Executive Summary" +author: "Ma'sum" +company: "Enterprise Corp" +coverPage: + enabled: true + preset: "modern" + badge: "CONFIDENTIAL" + footerText: "Proprietary & Confidential - 2026" +--- +# Executive Summary +Document contents after cover page. +`; + const doc = parseMarkdown(md); + const resolved = resolveDocumentConfig(doc.metadata as Record, {}); + + expect(resolved.coverPage).toBeDefined(); + expect(resolved.coverPage?.preset).toBe("modern"); + expect(resolved.coverPage?.badge).toBe("CONFIDENTIAL"); + expect(resolved.coverPage?.company).toBe("Enterprise Corp"); + + const html = await buildHtmlDocument(doc, {}); + expect(html).toContain("markforge-cover cover-modern"); + expect(html).toContain("CONFIDENTIAL"); + expect(html).toContain("Quarterly Financial Report"); + expect(html).toContain("Executive Summary"); + + const docxBuf = await buildDocxDocument(doc, {}); + expect(docxBuf.length).toBeGreaterThan(500); + }); + + it("Feature 2: Math & LaTeX Equation Rendering ($inline$ and $$block$$)", async () => { + const inlineRes = renderMathToHtml("E = mc^2", false); + expect(inlineRes).toContain("katex"); + + const blockRes = renderMathToHtml("\\int_0^\\infty e^{-x} dx = 1", true); + expect(blockRes).toContain("katex-display"); + + const md = `# Physics Formula\n\nThe mass-energy equivalence is $E = mc^2$.\n\n$$\\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}$$\n`; + const doc = parseMarkdown(md); + expect(doc.nodes.some((n) => n.type === "mathBlock")).toBe(true); + + const html = await buildHtmlDocument(doc, {}); + expect(html).toContain("katex"); + }); + + it("Feature 3: Multi-Column Container Directives (:::columns / :::col)", async () => { + const md = `# Multi Column Section + +:::columns 2 +:::col +### Column Left +This is the left column content. +::: +:::col +### Column Right +This is the right column content. +::: +::: +`; + const doc = parseMarkdown(md); + const colNode = doc.nodes.find((n) => n.type === "columns"); + expect(colNode).toBeDefined(); + expect(colNode?.columnsCount).toBe(2); + expect(colNode?.children?.length).toBe(2); + + const html = await buildHtmlDocument(doc, {}); + expect(html).toContain("markforge-columns"); + expect(html).toContain("Column Left"); + expect(html).toContain("Column Right"); + + const docxBuf = await buildDocxDocument(doc, {}); + expect(docxBuf.length).toBeGreaterThan(500); + }); + + it("Feature 4: Interactive Live-Reload Preview Server", async () => { + const sampleMdPath = path.join(tmpDir, "server-test.md"); + fs.writeFileSync(sampleMdPath, "# Hello Preview Server\nLive preview content.", "utf-8"); + + const serverInstance = await startPreviewServer({ + filePath: sampleMdPath, + port: 39123, + open: false, + }); + + expect(serverInstance.port).toBe(39123); + expect(serverInstance.url).toBe("http://localhost:39123"); + + // Fetch index Studio UI + const indexRes = await fetch("http://localhost:39123/"); + expect(indexRes.status).toBe(200); + const indexHtml = await indexRes.text(); + expect(indexHtml).toContain("MARKFORGE STUDIO"); + expect(indexHtml).toContain("Ma'sum"); + expect(indexHtml).toContain("code-editor"); + + // Test API: Get File Content + const getRes = await fetch("http://localhost:39123/api/file-content"); + expect(getRes.status).toBe(200); + const getData = (await getRes.json()) as { content: string; fileName: string }; + expect(getData.content).toContain("Hello Preview Server"); + + // Test API: Save File Content + const saveRes = await fetch("http://localhost:39123/api/save-content", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: "# Updated Content\n\nLive updated content." }), + }); + expect(saveRes.status).toBe(200); + const saveData = (await saveRes.json()) as { success: boolean }; + expect(saveData.success).toBe(true); + + // Fetch document content frame + const docRes = await fetch("http://localhost:39123/document-content"); + expect(docRes.status).toBe(200); + const docHtml = await docRes.text(); + expect(docHtml).toContain("Updated Content"); + expect(docHtml).toContain("Live updated content."); + + await serverInstance.close(); + }); + + it("Feature 5: Hierarchical Section Numbering (numberHeadings)", () => { + const directHeadings = normalizeNumberHeadings({ enabled: true, depth: 2, skipH1: true }); + expect(directHeadings?.enabled).toBe(true); + expect(directHeadings?.skipH1).toBe(true); + + const md = `--- +numberHeadings: + enabled: true + depth: 3 + skipH1: false +--- +# Introduction +## Background +### Details +# Methodology +## Approach +`; + const doc = parseMarkdown(md); + const headings = doc.nodes.filter((n) => n.type === "heading"); + + expect(headings[0].text).toBe("1. Introduction"); + expect(headings[1].text).toBe("1.1. Background"); + expect(headings[2].text).toBe("1.1.1. Details"); + expect(headings[3].text).toBe("2. Methodology"); + expect(headings[4].text).toBe("2.1. Approach"); + + // Check TOC entries match numbering + expect(doc.tocEntries[0].text).toBe("1. Introduction"); + expect(doc.tocEntries[1].text).toBe("1.1. Background"); + }); + + it("Feature 6: Footnotes & Endnotes ([^1] and [^1]: text)", async () => { + const md = `Here is a statement with a footnote[^alpha] and another one[^beta]. + +[^alpha]: This is the first footnote explanation. +[^beta]: This is the second footnote explanation with **bold** text. +`; + const doc = parseMarkdown(md); + expect(doc.footnoteDefs.length).toBe(2); + expect(doc.footnoteDefs[0].id).toBe("alpha"); + expect(doc.footnoteDefs[1].id).toBe("beta"); + + const html = await buildHtmlDocument(doc, {}); + expect(html).toContain('class="markforge-fnref"'); + expect(html).toContain('class="markforge-footnotes"'); + expect(html).toContain("first footnote explanation"); + expect(html).toContain("second footnote explanation"); + + const docxBuf = await buildDocxDocument(doc, {}); + expect(docxBuf.length).toBeGreaterThan(500); + }); + + it("Feature 7: PDF Security & Metadata Normalization", async () => { + const sec = normalizeSecurity({ + userPassword: "user123", + ownerPassword: "owner123", + permissions: { printing: "highResolution", copying: false }, + }); + + expect(sec).toBeDefined(); + expect(sec?.userPassword).toBe("user123"); + expect(sec?.permissions?.copying).toBe(false); + + const md = `# Document Security Test\nContent protected by policy.\n`; + const doc = parseMarkdown(md); + const pdfBuf = await buildPdfDocument(doc, { + metadata: { title: "Secure Doc", author: "Ma'sum" }, + security: sec, + }); + + expect(pdfBuf.length).toBeGreaterThan(500); + }); + + it("Feature 8: Back Cover / Closing Page Builder (Resolution & HTML/DOCX)", async () => { + const directBack = normalizeBackCover({ + enabled: true, + preset: "corporate", + title: "Contact & Legal Disclosures", + email: "contact@masumdev.com", + website: "https://masumdev.com", + company: "Masum Dev Technologies", + social: { github: "https://github.com/masumrpg" }, + }); + + expect(directBack).toBeDefined(); + expect(directBack?.preset).toBe("corporate"); + expect(directBack?.title).toBe("Contact & Legal Disclosures"); + expect(directBack?.email).toBe("contact@masumdev.com"); + expect(directBack?.social?.github).toBe("https://github.com/masumrpg"); + expect(directBack?.copyright).toContain("Masum Dev Technologies"); + + const md = `--- +title: "Project Alpha Final Proposal" +author: "Ma'sum" +company: "Masum Dev Technologies" +backCover: + enabled: true + preset: "modern" + title: "Thank You For Your Partnership" + subtitle: "We look forward to transforming mobile experiences together." + email: "hello@masumdev.com" + phone: "+62 812 3456 7890" + address: "Jakarta, Indonesia" + website: "https://masumdev.com" + social: + github: "https://github.com/masumrpg" + twitter: "https://twitter.com/masumrpg" +--- +# Main Content +Body paragraph before closing page. +`; + const doc = parseMarkdown(md); + const resolved = resolveDocumentConfig(doc.metadata as Record, {}); + + expect(resolved.backCover).toBeDefined(); + expect(resolved.backCover?.enabled).toBe(true); + expect(resolved.backCover?.preset).toBe("modern"); + expect(resolved.backCover?.email).toBe("hello@masumdev.com"); + + const html = await buildHtmlDocument(doc, {}); + expect(html).toContain('class="markforge-back-cover back-modern"'); + expect(html).toContain("Thank You For Your Partnership"); + expect(html).toContain("hello@masumdev.com"); + expect(html).toContain("https://github.com/masumrpg"); + + const docxBuf = await buildDocxDocument(doc, {}); + expect(docxBuf.length).toBeGreaterThan(1000); + }); + + it("Metadata Cascading Priority: Frontmatter vs Config File", () => { + const configFromDisk = { + title: "Config Default Title", + author: "Config Author", + company: "Masum Dev Technologies Corp", + version: "1.0.0", + lang: "en", + }; + + const frontmatterFromDoc = { + title: "Overridden Frontmatter Title", + author: "Ma'sum RPG", + // Notice: company, version, and lang are omitted from frontmatter + }; + + const resolved = resolveDocumentConfig(frontmatterFromDoc, { + metadata: configFromDisk, + }); + + // 1. Frontmatter overrides config title & author + expect(resolved.title).toBe("Overridden Frontmatter Title"); + expect(resolved.author).toBe("Ma'sum RPG"); + + // 2. Config fills in omitted company, version, and lang + expect(resolved.company).toBe("Masum Dev Technologies Corp"); + expect(resolved.version).toBe("1.0.0"); + expect(resolved.lang).toBe("en"); + }); +});