@masumdev/markforge 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -165,43 +165,133 @@ footer:
165
165
 
166
166
  ---
167
167
 
168
- ## ⚙️ Configuration (`markforge.config.ts`)
168
+ ## ⚙️ Configuration Reference
169
+
170
+ MarkForge automatically discovers configuration files starting from the input file's directory up to the workspace root, or via the `-c, --config <path>` CLI flag.
171
+
172
+ ### 1. TypeScript (`markforge.config.ts`)
169
173
 
170
174
  ```typescript
171
175
  import { defineConfig } from "@masumdev/markforge";
172
176
 
173
177
  export default defineConfig({
178
+ // Output formats: "docx" | "pdf" | "html"
174
179
  to: ["docx", "pdf", "html"],
175
180
  outputDir: "./dist/documents",
176
- theme: "default",
177
- orientation: "portrait",
178
- paperSize: "A4",
181
+
182
+ // Theme: "default" | "academic" | "github" | "corporate" | "minimal" | "dracula"
183
+ theme: "academic",
184
+
185
+ // Custom CSS stylesheet(s) to inject
186
+ css: ["./styles/custom.css"],
187
+
188
+ // Page layout & dimensions
189
+ orientation: "portrait", // "portrait" | "landscape"
190
+ paperSize: "A4", // "A4" | "Letter" | "Legal" | "A3" | "A5"
179
191
  margins: {
180
192
  top: "2.5cm",
181
193
  bottom: "2.5cm",
182
194
  left: "3cm",
183
195
  right: "3cm",
184
196
  },
197
+
198
+ // Multi-zone header & footer (supports tokens: {title}, {author}, {version}, {date}, {page}, {pages})
185
199
  header: {
186
- left: "My Organization",
200
+ left: "Enterprise Architecture",
187
201
  center: "{title}",
188
202
  right: "v{version}",
189
203
  },
190
204
  footer: {
191
- left: "Confidential",
205
+ left: "Confidential — Internal Use Only",
192
206
  right: "Page {page} of {pages}",
193
207
  },
194
- toc: true,
195
- embedImages: true,
196
- watermark: {
197
- enabled: false, // disabled by default enable to add branding
198
- text: "DRAFT",
199
- opacity: 0.07,
200
- position: "diagonal",
208
+
209
+ // Document features
210
+ toc: true, // Auto-generate Table of Contents
211
+ embedImages: true, // Embed/inline all images as Base64 data URIs
212
+ bundleHtml: true, // Self-contained HTML with embedded styles and scripts
213
+ syntaxTheme: "github-dark", // "github-dark" | "github-light" | "dracula" | "monokai" | "nord"
214
+
215
+ // Diagonal page watermark
216
+ watermark: "CONFIDENTIAL",
217
+
218
+ // Default document metadata
219
+ metadata: {
220
+ title: "System Architecture Specification",
221
+ author: "Ma'sum",
222
+ version: "1.0.0",
223
+ company: "My Organization",
201
224
  },
225
+
226
+ // Server & watch options
227
+ watch: false,
228
+ serve: false,
229
+ port: 4000,
230
+ open: false,
202
231
  });
203
232
  ```
204
233
 
234
+ ### 2. JSON with `$schema` (`markforge.config.json`)
235
+
236
+ Adding `$schema` enables **instant autocompletion and validation** in VS Code, WebStorm, and other IDEs:
237
+
238
+ ```json
239
+ {
240
+ "$schema": "https://raw.githubusercontent.com/masumdev/react-native-library/main/packages/markforge/schema.json",
241
+ "to": ["docx", "pdf", "html"],
242
+ "outputDir": "./dist/documents",
243
+ "theme": "academic",
244
+ "orientation": "portrait",
245
+ "paperSize": "A4",
246
+ "margins": {
247
+ "top": "2.5cm",
248
+ "bottom": "2.5cm",
249
+ "left": "3cm",
250
+ "right": "3cm"
251
+ },
252
+ "header": {
253
+ "left": "Enterprise Architecture",
254
+ "right": "{title}"
255
+ },
256
+ "footer": {
257
+ "left": "Confidential",
258
+ "right": "Page {page} of {pages}"
259
+ },
260
+ "toc": true,
261
+ "embedImages": true,
262
+ "syntaxTheme": "github-dark",
263
+ "watermark": "CONFIDENTIAL"
264
+ }
265
+ ```
266
+
267
+ ### 3. YAML (`markforge.config.yaml` / `.markforgerc.yaml`)
268
+
269
+ ```yaml
270
+ to:
271
+ - docx
272
+ - pdf
273
+ - html
274
+ outputDir: ./dist/documents
275
+ theme: academic
276
+ orientation: portrait
277
+ paperSize: A4
278
+ margins:
279
+ top: 2.5cm
280
+ bottom: 2.5cm
281
+ left: 3cm
282
+ right: 3cm
283
+ header:
284
+ left: Enterprise Architecture
285
+ right: "{title}"
286
+ footer:
287
+ left: Confidential
288
+ right: "Page {page} of {pages}"
289
+ toc: true
290
+ embedImages: true
291
+ watermark: CONFIDENTIAL
292
+ ```
293
+
294
+
205
295
  ---
206
296
 
207
297
  ## 💻 Programmatic API
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-NGC2ZAWZ.mjs";
13
13
  import {
14
14
  DEFAULT_CONFIG
15
- } from "./chunk-ASXSHC6H.mjs";
15
+ } from "./chunk-IYGPJIK5.mjs";
16
16
  import "./chunk-YZHGBG4N.mjs";
17
17
 
18
18
  // src/ui/App.tsx
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ try {
3
+ if (typeof globalThis !== "undefined" && (!globalThis.localStorage || typeof globalThis.localStorage.getItem !== "function")) {
4
+ Object.defineProperty(globalThis, "localStorage", {
5
+ value: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, key: () => null, length: 0 },
6
+ configurable: true, writable: true,
7
+ });
8
+ }
9
+ } catch {}
10
+ import {
11
+ __require
12
+ } from "./chunk-YZHGBG4N.mjs";
13
+
14
+ // src/config/loadConfig.ts
15
+ import * as fs from "fs";
16
+ import * as path from "path";
17
+ import { pathToFileURL } from "url";
18
+ import * as YAML from "yaml";
19
+ var DEFAULT_CONFIG_FILENAMES = [
20
+ "markforge.config.json",
21
+ ".markforgerc.json",
22
+ "markforge.config.yaml",
23
+ "markforge.config.yml",
24
+ ".markforgerc.yaml",
25
+ ".markforgerc.yml",
26
+ ".markforgerc",
27
+ "markforge.config.ts",
28
+ "markforge.config.js",
29
+ "markforge.config.mjs",
30
+ "markforge.config.cjs"
31
+ ];
32
+ var DEFAULT_CONFIG = {
33
+ to: ["docx", "pdf"],
34
+ outputDir: void 0,
35
+ theme: "default",
36
+ css: void 0,
37
+ orientation: "portrait",
38
+ paperSize: "A4",
39
+ margins: {
40
+ top: "2.5cm",
41
+ bottom: "2.5cm",
42
+ left: "2.5cm",
43
+ right: "2.5cm"
44
+ },
45
+ header: void 0,
46
+ footer: {
47
+ right: "Page {page} of {pages}"
48
+ },
49
+ toc: false,
50
+ watermark: void 0,
51
+ embedImages: true,
52
+ metadata: void 0,
53
+ watch: false,
54
+ serve: false,
55
+ port: 4e3,
56
+ open: false,
57
+ bundleHtml: true,
58
+ syntaxTheme: "github-dark"
59
+ };
60
+ function discoverConfigFile(startDir) {
61
+ const dirsToCheck = [];
62
+ let curr = path.resolve(startDir);
63
+ while (curr) {
64
+ dirsToCheck.push(curr);
65
+ const parent = path.dirname(curr);
66
+ if (parent === curr) break;
67
+ curr = parent;
68
+ }
69
+ const cwd = path.resolve(process.cwd());
70
+ if (!dirsToCheck.includes(cwd)) {
71
+ dirsToCheck.push(cwd);
72
+ }
73
+ for (const dir of dirsToCheck) {
74
+ for (const filename of DEFAULT_CONFIG_FILENAMES) {
75
+ const candidate = path.join(dir, filename);
76
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
77
+ return candidate;
78
+ }
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+ async function loadConfig(customPath, startDir = process.cwd()) {
84
+ let resolvedPath = null;
85
+ if (customPath) {
86
+ resolvedPath = path.isAbsolute(customPath) ? customPath : path.resolve(process.cwd(), customPath);
87
+ if (!fs.existsSync(resolvedPath)) {
88
+ const altPath = path.resolve(startDir, customPath);
89
+ if (fs.existsSync(altPath)) {
90
+ resolvedPath = altPath;
91
+ } else {
92
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
93
+ }
94
+ }
95
+ } else {
96
+ resolvedPath = discoverConfigFile(startDir);
97
+ }
98
+ if (!resolvedPath) {
99
+ return {
100
+ config: { ...DEFAULT_CONFIG },
101
+ configPath: null
102
+ };
103
+ }
104
+ const ext = path.extname(resolvedPath).toLowerCase();
105
+ let userConfig = {};
106
+ try {
107
+ if (ext === ".json" || ext === "" || resolvedPath.endsWith(".markforgerc")) {
108
+ const raw = fs.readFileSync(resolvedPath, "utf-8").trim();
109
+ try {
110
+ userConfig = JSON.parse(raw);
111
+ } catch {
112
+ userConfig = YAML.parse(raw) || {};
113
+ }
114
+ } else if (ext === ".yaml" || ext === ".yml") {
115
+ const raw = fs.readFileSync(resolvedPath, "utf-8");
116
+ userConfig = YAML.parse(raw) || {};
117
+ } else if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
118
+ try {
119
+ const fileUrl = `${pathToFileURL(resolvedPath).href}?t=${Date.now()}`;
120
+ const mod = await import(fileUrl);
121
+ const rawExport = mod.default ?? mod.config ?? mod;
122
+ userConfig = (typeof rawExport === "function" ? await rawExport() : rawExport) || {};
123
+ } catch (importErr) {
124
+ try {
125
+ const mod = __require(resolvedPath);
126
+ const rawExport = mod.default ?? mod.config ?? mod;
127
+ userConfig = (typeof rawExport === "function" ? await rawExport() : rawExport) || {};
128
+ } catch {
129
+ throw importErr;
130
+ }
131
+ }
132
+ }
133
+ } catch (err) {
134
+ throw new Error(
135
+ `Failed to parse configuration file at ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`
136
+ );
137
+ }
138
+ if (userConfig && typeof userConfig === "object" && "$schema" in userConfig) {
139
+ delete userConfig.$schema;
140
+ }
141
+ const parsedConfig = userConfig;
142
+ const mergedConfig = {
143
+ ...DEFAULT_CONFIG,
144
+ ...parsedConfig,
145
+ margins: {
146
+ ...DEFAULT_CONFIG.margins,
147
+ ...parsedConfig.margins || {}
148
+ },
149
+ header: parsedConfig.header !== void 0 ? parsedConfig.header : DEFAULT_CONFIG.header,
150
+ footer: parsedConfig.footer !== void 0 ? parsedConfig.footer : DEFAULT_CONFIG.footer,
151
+ metadata: {
152
+ ...DEFAULT_CONFIG.metadata || {},
153
+ ...parsedConfig.metadata || {}
154
+ }
155
+ };
156
+ return {
157
+ config: mergedConfig,
158
+ configPath: resolvedPath
159
+ };
160
+ }
161
+
162
+ export {
163
+ DEFAULT_CONFIG,
164
+ loadConfig
165
+ };
package/dist/cli.mjs CHANGED
@@ -45,7 +45,7 @@ process.on("warning", (w) => {
45
45
  });
46
46
  async function main() {
47
47
  const program = new Command();
48
- program.name("markforge").description("Modern Markdown & MDX multi-format publishing engine & CLI (DOCX, PDF, HTML, Images)").version(MARKFORGE_VERSION, "-V, --version", "Output current markforge version").argument("[input-file]", "Markdown or MDX source file to compile", "README.md").option("-t, --to <formats>", "Target formats to compile to (comma-separated: docx,pdf,html)", "docx,pdf").option("-o, --output <dir>", "Output directory for generated documents").option("--theme <theme>", "Built-in theme name (default, academic, github, corporate, minimal)").option("--css <path...>", "Custom CSS stylesheet(s) to inject").option("-c, --config <path>", "Path to custom markforge configuration file").option("--toc", "Force Table of Contents generation").option("-w, --watch", "Watch input file for changes and recompile", false).option("-s, --serve [port]", "Start local HTTP preview server", false).option("-O, --open", "Open compiled document in default browser", false);
48
+ program.name("markforge").description("Modern Markdown & MDX multi-format publishing engine & CLI (DOCX, PDF, HTML, Images)").version(MARKFORGE_VERSION, "-V, --version", "Output current markforge version").argument("[input-file]", "Markdown or MDX source file to compile", "README.md").option("-t, --to <formats>", "Target formats to compile to (comma-separated: docx,pdf,html)").option("-o, --output <dir>", "Output directory for generated documents").option("--theme <theme>", "Built-in theme name (default, academic, github, corporate, minimal)").option("--css <path...>", "Custom CSS stylesheet(s) to inject").option("-c, --config <path>", "Path to custom markforge configuration file").option("--toc", "Force Table of Contents generation").option("-w, --watch", "Watch input file for changes and recompile", false).option("-s, --serve [port]", "Start local HTTP preview server", false).option("-O, --open", "Open compiled document in default browser", false);
49
49
  program.parse(process.argv);
50
50
  const options = program.opts();
51
51
  const inputArg = program.args[0] || "README.md";
@@ -54,16 +54,19 @@ async function main() {
54
54
  console.error(`[ERROR] File not found: ${resolvedInputPath}`);
55
55
  process.exit(1);
56
56
  }
57
- const rawFormats = (options.to || "docx,pdf").split(",").map((f) => f.trim().toLowerCase()).filter(Boolean);
57
+ const cliFormats = options.to ? options.to.split(",").map((f) => f.trim().toLowerCase()).filter(Boolean) : void 0;
58
58
  const [{ render }, { App }, { loadConfig }] = await Promise.all([
59
59
  import("ink"),
60
- import("./App-DKCNXX3Z.mjs"),
61
- import("./loadConfig-T4ZV6YY2.mjs")
60
+ import("./App-IJVCGBJM.mjs"),
61
+ import("./loadConfig-PD6ENMAM.mjs")
62
62
  ]);
63
- const fileConfig = await loadConfig(options.config, path.dirname(resolvedInputPath));
63
+ const { config: fileConfig } = await loadConfig(
64
+ options.config,
65
+ path.dirname(resolvedInputPath)
66
+ );
64
67
  const mergedConfig = {
65
68
  ...fileConfig,
66
- to: rawFormats.length > 0 ? rawFormats : fileConfig.to ?? ["docx", "pdf"],
69
+ to: cliFormats && cliFormats.length > 0 ? cliFormats : fileConfig.to ?? ["docx", "pdf"],
67
70
  ...options.output ? { outputDir: options.output } : {},
68
71
  ...options.theme ? { theme: options.theme } : {},
69
72
  ...options.css ? { css: options.css } : {},
package/dist/index.d.mts CHANGED
@@ -333,7 +333,7 @@ declare const DEFAULT_CONFIG: Required<Omit<MarkforgeConfig, "outputDir" | "css"
333
333
  /**
334
334
  * Resolves and loads markforge configuration from disk.
335
335
  */
336
- declare function loadConfig(customPath?: string, cwd?: string): Promise<{
336
+ declare function loadConfig(customPath?: string, startDir?: string): Promise<{
337
337
  config: MarkforgeConfig;
338
338
  configPath: string | null;
339
339
  }>;
package/dist/index.d.ts CHANGED
@@ -333,7 +333,7 @@ declare const DEFAULT_CONFIG: Required<Omit<MarkforgeConfig, "outputDir" | "css"
333
333
  /**
334
334
  * Resolves and loads markforge configuration from disk.
335
335
  */
336
- declare function loadConfig(customPath?: string, cwd?: string): Promise<{
336
+ declare function loadConfig(customPath?: string, startDir?: string): Promise<{
337
337
  config: MarkforgeConfig;
338
338
  configPath: string | null;
339
339
  }>;
package/dist/index.js CHANGED
@@ -2302,21 +2302,43 @@ var DEFAULT_CONFIG = {
2302
2302
  bundleHtml: true,
2303
2303
  syntaxTheme: "github-dark"
2304
2304
  };
2305
- async function loadConfig(customPath, cwd = process.cwd()) {
2305
+ function discoverConfigFile(startDir) {
2306
+ const dirsToCheck = [];
2307
+ let curr = path5.resolve(startDir);
2308
+ while (curr) {
2309
+ dirsToCheck.push(curr);
2310
+ const parent = path5.dirname(curr);
2311
+ if (parent === curr) break;
2312
+ curr = parent;
2313
+ }
2314
+ const cwd = path5.resolve(process.cwd());
2315
+ if (!dirsToCheck.includes(cwd)) {
2316
+ dirsToCheck.push(cwd);
2317
+ }
2318
+ for (const dir of dirsToCheck) {
2319
+ for (const filename of DEFAULT_CONFIG_FILENAMES) {
2320
+ const candidate = path5.join(dir, filename);
2321
+ if (fs5.existsSync(candidate) && fs5.statSync(candidate).isFile()) {
2322
+ return candidate;
2323
+ }
2324
+ }
2325
+ }
2326
+ return null;
2327
+ }
2328
+ async function loadConfig(customPath, startDir = process.cwd()) {
2306
2329
  let resolvedPath = null;
2307
2330
  if (customPath) {
2308
- resolvedPath = path5.isAbsolute(customPath) ? customPath : path5.resolve(cwd, customPath);
2331
+ resolvedPath = path5.isAbsolute(customPath) ? customPath : path5.resolve(process.cwd(), customPath);
2309
2332
  if (!fs5.existsSync(resolvedPath)) {
2310
- throw new Error(`Configuration file not found: ${resolvedPath}`);
2311
- }
2312
- } else {
2313
- for (const filename of DEFAULT_CONFIG_FILENAMES) {
2314
- const candidate = path5.resolve(cwd, filename);
2315
- if (fs5.existsSync(candidate)) {
2316
- resolvedPath = candidate;
2317
- break;
2333
+ const altPath = path5.resolve(startDir, customPath);
2334
+ if (fs5.existsSync(altPath)) {
2335
+ resolvedPath = altPath;
2336
+ } else {
2337
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
2318
2338
  }
2319
2339
  }
2340
+ } else {
2341
+ resolvedPath = discoverConfigFile(startDir);
2320
2342
  }
2321
2343
  if (!resolvedPath) {
2322
2344
  return {
@@ -2326,32 +2348,55 @@ async function loadConfig(customPath, cwd = process.cwd()) {
2326
2348
  }
2327
2349
  const ext = path5.extname(resolvedPath).toLowerCase();
2328
2350
  let userConfig = {};
2329
- if (ext === ".json" || resolvedPath.endsWith(".markforgerc")) {
2330
- const raw = fs5.readFileSync(resolvedPath, "utf-8");
2331
- userConfig = JSON.parse(raw);
2332
- } else if (ext === ".yaml" || ext === ".yml") {
2333
- const raw = fs5.readFileSync(resolvedPath, "utf-8");
2334
- userConfig = YAML.parse(raw);
2335
- } else if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
2336
- try {
2337
- const fileUrl = (0, import_node_url3.pathToFileURL)(resolvedPath).href;
2338
- const mod = await import(fileUrl);
2339
- userConfig = mod.default || mod;
2340
- } catch {
2341
- const required = require(resolvedPath);
2342
- userConfig = required.default || required;
2351
+ try {
2352
+ if (ext === ".json" || ext === "" || resolvedPath.endsWith(".markforgerc")) {
2353
+ const raw = fs5.readFileSync(resolvedPath, "utf-8").trim();
2354
+ try {
2355
+ userConfig = JSON.parse(raw);
2356
+ } catch {
2357
+ userConfig = YAML.parse(raw) || {};
2358
+ }
2359
+ } else if (ext === ".yaml" || ext === ".yml") {
2360
+ const raw = fs5.readFileSync(resolvedPath, "utf-8");
2361
+ userConfig = YAML.parse(raw) || {};
2362
+ } else if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
2363
+ try {
2364
+ const fileUrl = `${(0, import_node_url3.pathToFileURL)(resolvedPath).href}?t=${Date.now()}`;
2365
+ const mod = await import(fileUrl);
2366
+ const rawExport = mod.default ?? mod.config ?? mod;
2367
+ userConfig = (typeof rawExport === "function" ? await rawExport() : rawExport) || {};
2368
+ } catch (importErr) {
2369
+ try {
2370
+ const mod = require(resolvedPath);
2371
+ const rawExport = mod.default ?? mod.config ?? mod;
2372
+ userConfig = (typeof rawExport === "function" ? await rawExport() : rawExport) || {};
2373
+ } catch {
2374
+ throw importErr;
2375
+ }
2376
+ }
2343
2377
  }
2378
+ } catch (err) {
2379
+ throw new Error(
2380
+ `Failed to parse configuration file at ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`
2381
+ );
2382
+ }
2383
+ if (userConfig && typeof userConfig === "object" && "$schema" in userConfig) {
2384
+ delete userConfig.$schema;
2344
2385
  }
2386
+ const parsedConfig = userConfig;
2345
2387
  const mergedConfig = {
2346
2388
  ...DEFAULT_CONFIG,
2347
- ...userConfig,
2389
+ ...parsedConfig,
2348
2390
  margins: {
2349
2391
  ...DEFAULT_CONFIG.margins,
2350
- ...userConfig.margins
2392
+ ...parsedConfig.margins || {}
2351
2393
  },
2352
- header: userConfig.header || DEFAULT_CONFIG.header,
2353
- footer: userConfig.footer || DEFAULT_CONFIG.footer,
2354
- metadata: userConfig.metadata
2394
+ header: parsedConfig.header !== void 0 ? parsedConfig.header : DEFAULT_CONFIG.header,
2395
+ footer: parsedConfig.footer !== void 0 ? parsedConfig.footer : DEFAULT_CONFIG.footer,
2396
+ metadata: {
2397
+ ...DEFAULT_CONFIG.metadata || {},
2398
+ ...parsedConfig.metadata || {}
2399
+ }
2355
2400
  };
2356
2401
  return {
2357
2402
  config: mergedConfig,
package/dist/index.mjs CHANGED
@@ -2267,21 +2267,43 @@ var DEFAULT_CONFIG = {
2267
2267
  bundleHtml: true,
2268
2268
  syntaxTheme: "github-dark"
2269
2269
  };
2270
- async function loadConfig(customPath, cwd = process.cwd()) {
2270
+ function discoverConfigFile(startDir) {
2271
+ const dirsToCheck = [];
2272
+ let curr = path5.resolve(startDir);
2273
+ while (curr) {
2274
+ dirsToCheck.push(curr);
2275
+ const parent = path5.dirname(curr);
2276
+ if (parent === curr) break;
2277
+ curr = parent;
2278
+ }
2279
+ const cwd = path5.resolve(process.cwd());
2280
+ if (!dirsToCheck.includes(cwd)) {
2281
+ dirsToCheck.push(cwd);
2282
+ }
2283
+ for (const dir of dirsToCheck) {
2284
+ for (const filename of DEFAULT_CONFIG_FILENAMES) {
2285
+ const candidate = path5.join(dir, filename);
2286
+ if (fs5.existsSync(candidate) && fs5.statSync(candidate).isFile()) {
2287
+ return candidate;
2288
+ }
2289
+ }
2290
+ }
2291
+ return null;
2292
+ }
2293
+ async function loadConfig(customPath, startDir = process.cwd()) {
2271
2294
  let resolvedPath = null;
2272
2295
  if (customPath) {
2273
- resolvedPath = path5.isAbsolute(customPath) ? customPath : path5.resolve(cwd, customPath);
2296
+ resolvedPath = path5.isAbsolute(customPath) ? customPath : path5.resolve(process.cwd(), customPath);
2274
2297
  if (!fs5.existsSync(resolvedPath)) {
2275
- throw new Error(`Configuration file not found: ${resolvedPath}`);
2276
- }
2277
- } else {
2278
- for (const filename of DEFAULT_CONFIG_FILENAMES) {
2279
- const candidate = path5.resolve(cwd, filename);
2280
- if (fs5.existsSync(candidate)) {
2281
- resolvedPath = candidate;
2282
- break;
2298
+ const altPath = path5.resolve(startDir, customPath);
2299
+ if (fs5.existsSync(altPath)) {
2300
+ resolvedPath = altPath;
2301
+ } else {
2302
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
2283
2303
  }
2284
2304
  }
2305
+ } else {
2306
+ resolvedPath = discoverConfigFile(startDir);
2285
2307
  }
2286
2308
  if (!resolvedPath) {
2287
2309
  return {
@@ -2291,32 +2313,55 @@ async function loadConfig(customPath, cwd = process.cwd()) {
2291
2313
  }
2292
2314
  const ext = path5.extname(resolvedPath).toLowerCase();
2293
2315
  let userConfig = {};
2294
- if (ext === ".json" || resolvedPath.endsWith(".markforgerc")) {
2295
- const raw = fs5.readFileSync(resolvedPath, "utf-8");
2296
- userConfig = JSON.parse(raw);
2297
- } else if (ext === ".yaml" || ext === ".yml") {
2298
- const raw = fs5.readFileSync(resolvedPath, "utf-8");
2299
- userConfig = YAML.parse(raw);
2300
- } else if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
2301
- try {
2302
- const fileUrl = pathToFileURL3(resolvedPath).href;
2303
- const mod = await import(fileUrl);
2304
- userConfig = mod.default || mod;
2305
- } catch {
2306
- const required = __require(resolvedPath);
2307
- userConfig = required.default || required;
2316
+ try {
2317
+ if (ext === ".json" || ext === "" || resolvedPath.endsWith(".markforgerc")) {
2318
+ const raw = fs5.readFileSync(resolvedPath, "utf-8").trim();
2319
+ try {
2320
+ userConfig = JSON.parse(raw);
2321
+ } catch {
2322
+ userConfig = YAML.parse(raw) || {};
2323
+ }
2324
+ } else if (ext === ".yaml" || ext === ".yml") {
2325
+ const raw = fs5.readFileSync(resolvedPath, "utf-8");
2326
+ userConfig = YAML.parse(raw) || {};
2327
+ } else if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
2328
+ try {
2329
+ const fileUrl = `${pathToFileURL3(resolvedPath).href}?t=${Date.now()}`;
2330
+ const mod = await import(fileUrl);
2331
+ const rawExport = mod.default ?? mod.config ?? mod;
2332
+ userConfig = (typeof rawExport === "function" ? await rawExport() : rawExport) || {};
2333
+ } catch (importErr) {
2334
+ try {
2335
+ const mod = __require(resolvedPath);
2336
+ const rawExport = mod.default ?? mod.config ?? mod;
2337
+ userConfig = (typeof rawExport === "function" ? await rawExport() : rawExport) || {};
2338
+ } catch {
2339
+ throw importErr;
2340
+ }
2341
+ }
2308
2342
  }
2343
+ } catch (err) {
2344
+ throw new Error(
2345
+ `Failed to parse configuration file at ${resolvedPath}: ${err instanceof Error ? err.message : String(err)}`
2346
+ );
2347
+ }
2348
+ if (userConfig && typeof userConfig === "object" && "$schema" in userConfig) {
2349
+ delete userConfig.$schema;
2309
2350
  }
2351
+ const parsedConfig = userConfig;
2310
2352
  const mergedConfig = {
2311
2353
  ...DEFAULT_CONFIG,
2312
- ...userConfig,
2354
+ ...parsedConfig,
2313
2355
  margins: {
2314
2356
  ...DEFAULT_CONFIG.margins,
2315
- ...userConfig.margins
2357
+ ...parsedConfig.margins || {}
2316
2358
  },
2317
- header: userConfig.header || DEFAULT_CONFIG.header,
2318
- footer: userConfig.footer || DEFAULT_CONFIG.footer,
2319
- metadata: userConfig.metadata
2359
+ header: parsedConfig.header !== void 0 ? parsedConfig.header : DEFAULT_CONFIG.header,
2360
+ footer: parsedConfig.footer !== void 0 ? parsedConfig.footer : DEFAULT_CONFIG.footer,
2361
+ metadata: {
2362
+ ...DEFAULT_CONFIG.metadata || {},
2363
+ ...parsedConfig.metadata || {}
2364
+ }
2320
2365
  };
2321
2366
  return {
2322
2367
  config: mergedConfig,
@@ -10,7 +10,7 @@ try {
10
10
  import {
11
11
  DEFAULT_CONFIG,
12
12
  loadConfig
13
- } from "./chunk-ASXSHC6H.mjs";
13
+ } from "./chunk-IYGPJIK5.mjs";
14
14
  import "./chunk-YZHGBG4N.mjs";
15
15
  export {
16
16
  DEFAULT_CONFIG,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masumdev/markforge",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Modern, high-performance Markdown & MDX multi-format document publishing engine and CLI (DOCX, PDF, HTML, and Images) with embedded HTML/CSS, image inliner, and Ink terminal UI.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
package/schema.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "$schema": "http://json-schema.org/draft-07/schema#",
3
3
  "title": "MarkforgeConfig",
4
- "description": "Configuration schema for @masumdev/markforge Markdown & MDX document publishing engine",
4
+ "description": "Type-safe configuration schema for @masumdev/markforge Markdown & MDX document publishing engine",
5
5
  "type": "object",
6
6
  "properties": {
7
7
  "$schema": {
8
8
  "type": "string",
9
- "description": "JSON schema reference URI"
9
+ "description": "JSON Schema reference URI for IDE autocompletion and validation"
10
10
  },
11
11
  "to": {
12
- "description": "Target formats to compile markdown to",
12
+ "description": "Target formats to compile markdown to (docx, pdf, html, png)",
13
13
  "oneOf": [
14
14
  {
15
15
  "type": "string",
@@ -31,12 +31,12 @@
31
31
  },
32
32
  "theme": {
33
33
  "type": "string",
34
- "description": "Visual document theme",
34
+ "description": "Visual document theme name",
35
35
  "enum": ["default", "academic", "github", "corporate", "minimal", "dracula"],
36
36
  "default": "default"
37
37
  },
38
38
  "css": {
39
- "description": "Custom CSS stylesheet(s) to inject",
39
+ "description": "Custom CSS stylesheet path(s) to inject",
40
40
  "oneOf": [
41
41
  { "type": "string" },
42
42
  { "type": "array", "items": { "type": "string" } }
@@ -44,56 +44,87 @@
44
44
  },
45
45
  "orientation": {
46
46
  "type": "string",
47
+ "description": "Page orientation for PDF and DOCX",
47
48
  "enum": ["portrait", "landscape"],
48
49
  "default": "portrait"
49
50
  },
50
51
  "paperSize": {
51
52
  "type": "string",
53
+ "description": "Physical page size",
52
54
  "enum": ["A4", "Letter", "Legal", "A3", "A5"],
53
55
  "default": "A4"
54
56
  },
55
57
  "margins": {
56
58
  "type": "object",
59
+ "description": "Document margins (supports units like '2.5cm', '1in', '20mm', or pt numbers)",
57
60
  "properties": {
58
61
  "top": { "type": ["string", "number"] },
59
62
  "bottom": { "type": ["string", "number"] },
60
63
  "left": { "type": ["string", "number"] },
61
64
  "right": { "type": ["string", "number"] }
62
- }
65
+ },
66
+ "additionalProperties": false
63
67
  },
64
68
  "header": {
65
69
  "type": "object",
70
+ "description": "Header text across pages",
66
71
  "properties": {
67
72
  "left": { "type": "string" },
68
73
  "center": { "type": "string" },
69
74
  "right": { "type": "string" }
70
- }
75
+ },
76
+ "additionalProperties": false
71
77
  },
72
78
  "footer": {
73
79
  "type": "object",
80
+ "description": "Footer text across pages (supports {page} and {pages} variables)",
74
81
  "properties": {
75
82
  "left": { "type": "string" },
76
83
  "center": { "type": "string" },
77
84
  "right": { "type": "string" }
78
- }
85
+ },
86
+ "additionalProperties": false
79
87
  },
80
88
  "toc": {
81
89
  "type": "boolean",
82
- "description": "Auto-generate Table of Contents",
90
+ "description": "Auto-generate Table of Contents with page references",
83
91
  "default": false
84
92
  },
85
93
  "watermark": {
86
94
  "type": "string",
87
- "description": "Watermark text across pages"
95
+ "description": "Diagonal watermark text across document pages"
88
96
  },
89
97
  "embedImages": {
90
98
  "type": "boolean",
91
- "description": "Whether to inline/embed all images",
99
+ "description": "Whether to inline/embed all images as Base64 in outputs",
92
100
  "default": true
93
101
  },
102
+ "bundleHtml": {
103
+ "type": "boolean",
104
+ "description": "Produce self-contained HTML with embedded styles and scripts",
105
+ "default": true
106
+ },
107
+ "syntaxTheme": {
108
+ "type": "string",
109
+ "description": "Code block syntax highlighting color scheme",
110
+ "enum": ["github-dark", "github-light", "dracula", "monokai", "nord"],
111
+ "default": "github-dark"
112
+ },
113
+ "metadata": {
114
+ "type": "object",
115
+ "description": "Document frontmatter metadata defaults",
116
+ "properties": {
117
+ "title": { "type": "string" },
118
+ "author": { "type": "string" },
119
+ "date": { "type": "string" },
120
+ "version": { "type": "string" },
121
+ "company": { "type": "string" }
122
+ },
123
+ "additionalProperties": true
124
+ },
94
125
  "watch": {
95
126
  "type": "boolean",
96
- "description": "Watch mode for hot compilation",
127
+ "description": "Watch mode for re-compilation on source file changes",
97
128
  "default": false
98
129
  },
99
130
  "serve": {
@@ -108,7 +139,7 @@
108
139
  },
109
140
  "open": {
110
141
  "type": "boolean",
111
- "description": "Open document in browser on compile",
142
+ "description": "Automatically open document in browser on compile",
112
143
  "default": false
113
144
  }
114
145
  },
@@ -1,120 +0,0 @@
1
- #!/usr/bin/env node
2
- try {
3
- if (typeof globalThis !== "undefined" && (!globalThis.localStorage || typeof globalThis.localStorage.getItem !== "function")) {
4
- Object.defineProperty(globalThis, "localStorage", {
5
- value: { getItem: () => null, setItem: () => {}, removeItem: () => {}, clear: () => {}, key: () => null, length: 0 },
6
- configurable: true, writable: true,
7
- });
8
- }
9
- } catch {}
10
- import {
11
- __require
12
- } from "./chunk-YZHGBG4N.mjs";
13
-
14
- // src/config/loadConfig.ts
15
- import * as fs from "fs";
16
- import * as path from "path";
17
- import { pathToFileURL } from "url";
18
- import * as YAML from "yaml";
19
- var DEFAULT_CONFIG_FILENAMES = [
20
- "markforge.config.json",
21
- ".markforgerc.json",
22
- "markforge.config.yaml",
23
- "markforge.config.yml",
24
- ".markforgerc.yaml",
25
- ".markforgerc.yml",
26
- ".markforgerc",
27
- "markforge.config.ts",
28
- "markforge.config.js",
29
- "markforge.config.mjs",
30
- "markforge.config.cjs"
31
- ];
32
- var DEFAULT_CONFIG = {
33
- to: ["docx", "pdf"],
34
- outputDir: void 0,
35
- theme: "default",
36
- css: void 0,
37
- orientation: "portrait",
38
- paperSize: "A4",
39
- margins: {
40
- top: "2.5cm",
41
- bottom: "2.5cm",
42
- left: "2.5cm",
43
- right: "2.5cm"
44
- },
45
- header: void 0,
46
- footer: {
47
- right: "Page {page} of {pages}"
48
- },
49
- toc: false,
50
- watermark: void 0,
51
- embedImages: true,
52
- metadata: void 0,
53
- watch: false,
54
- serve: false,
55
- port: 4e3,
56
- open: false,
57
- bundleHtml: true,
58
- syntaxTheme: "github-dark"
59
- };
60
- async function loadConfig(customPath, cwd = process.cwd()) {
61
- let resolvedPath = null;
62
- if (customPath) {
63
- resolvedPath = path.isAbsolute(customPath) ? customPath : path.resolve(cwd, customPath);
64
- if (!fs.existsSync(resolvedPath)) {
65
- throw new Error(`Configuration file not found: ${resolvedPath}`);
66
- }
67
- } else {
68
- for (const filename of DEFAULT_CONFIG_FILENAMES) {
69
- const candidate = path.resolve(cwd, filename);
70
- if (fs.existsSync(candidate)) {
71
- resolvedPath = candidate;
72
- break;
73
- }
74
- }
75
- }
76
- if (!resolvedPath) {
77
- return {
78
- config: { ...DEFAULT_CONFIG },
79
- configPath: null
80
- };
81
- }
82
- const ext = path.extname(resolvedPath).toLowerCase();
83
- let userConfig = {};
84
- if (ext === ".json" || resolvedPath.endsWith(".markforgerc")) {
85
- const raw = fs.readFileSync(resolvedPath, "utf-8");
86
- userConfig = JSON.parse(raw);
87
- } else if (ext === ".yaml" || ext === ".yml") {
88
- const raw = fs.readFileSync(resolvedPath, "utf-8");
89
- userConfig = YAML.parse(raw);
90
- } else if (ext === ".ts" || ext === ".js" || ext === ".mjs" || ext === ".cjs") {
91
- try {
92
- const fileUrl = pathToFileURL(resolvedPath).href;
93
- const mod = await import(fileUrl);
94
- userConfig = mod.default || mod;
95
- } catch {
96
- const required = __require(resolvedPath);
97
- userConfig = required.default || required;
98
- }
99
- }
100
- const mergedConfig = {
101
- ...DEFAULT_CONFIG,
102
- ...userConfig,
103
- margins: {
104
- ...DEFAULT_CONFIG.margins,
105
- ...userConfig.margins
106
- },
107
- header: userConfig.header || DEFAULT_CONFIG.header,
108
- footer: userConfig.footer || DEFAULT_CONFIG.footer,
109
- metadata: userConfig.metadata
110
- };
111
- return {
112
- config: mergedConfig,
113
- configPath: resolvedPath
114
- };
115
- }
116
-
117
- export {
118
- DEFAULT_CONFIG,
119
- loadConfig
120
- };