@masumdev/markforge 0.2.5 → 0.3.0

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/dist/index.mjs CHANGED
@@ -1347,6 +1347,67 @@ function normalizeHeaderFooter(raw, meta) {
1347
1347
  dividerColor: raw.dividerColor || "#E2E8F0"
1348
1348
  };
1349
1349
  }
1350
+ function normalizeSignatures(raw, meta = {}) {
1351
+ if (!raw) return void 0;
1352
+ let rawItems = [];
1353
+ let rawConfig = {};
1354
+ if (Array.isArray(raw)) {
1355
+ rawItems = raw;
1356
+ } else if (typeof raw === "object" && Array.isArray(raw.items)) {
1357
+ rawItems = raw.items;
1358
+ rawConfig = raw;
1359
+ }
1360
+ if (rawItems.length === 0) return void 0;
1361
+ const cappedItems = rawItems.slice(0, 4);
1362
+ const items = cappedItems.map((item) => {
1363
+ const tokenCtx = meta;
1364
+ const rawName = typeof item.name === "string" ? item.name : "";
1365
+ const name = replaceDocumentTokens(rawName, tokenCtx).trim();
1366
+ const title = item.title ? replaceDocumentTokens(item.title, tokenCtx).trim() : void 0;
1367
+ const role = item.role ? replaceDocumentTokens(item.role, tokenCtx).trim() : void 0;
1368
+ let dateStr;
1369
+ if (typeof item.date === "string") {
1370
+ dateStr = replaceDocumentTokens(item.date, tokenCtx).trim();
1371
+ } else if (item.date === true) {
1372
+ dateStr = meta.date || (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
1373
+ }
1374
+ let signatureHeight = 60;
1375
+ if (typeof item.signatureHeight === "number") {
1376
+ signatureHeight = item.signatureHeight;
1377
+ } else if (typeof item.signatureHeight === "string") {
1378
+ const parsed = parseFloat(item.signatureHeight);
1379
+ if (!isNaN(parsed)) signatureHeight = parsed;
1380
+ }
1381
+ return {
1382
+ title,
1383
+ name: name || "Authorized Signatory",
1384
+ role,
1385
+ date: dateStr,
1386
+ image: item.image,
1387
+ signatureHeight
1388
+ };
1389
+ });
1390
+ const align = rawConfig.align || (items.length === 1 ? "right" : "space-between");
1391
+ const style = rawConfig.style || "line";
1392
+ const borderColor = rawConfig.borderColor || "#CBD5E1";
1393
+ const titleColor = rawConfig.titleColor || "#64748B";
1394
+ const nameColor = rawConfig.nameColor || "#0F172A";
1395
+ const roleColor = rawConfig.roleColor || "#64748B";
1396
+ const spacingBeforeRaw = rawConfig.spacingBefore ?? "2.5rem";
1397
+ const spacingBefore = formatMarginCss(spacingBeforeRaw, "2.5rem");
1398
+ const spacingBeforeTwip = parseMarginToTwip(spacingBeforeRaw, 600);
1399
+ return {
1400
+ items,
1401
+ align,
1402
+ style,
1403
+ borderColor,
1404
+ titleColor,
1405
+ nameColor,
1406
+ roleColor,
1407
+ spacingBefore,
1408
+ spacingBeforeTwip
1409
+ };
1410
+ }
1350
1411
  function resolveDocumentConfig(frontmatter = {}, userConfig = {}) {
1351
1412
  const configMeta = userConfig.metadata || {};
1352
1413
  const mergedMeta = { ...configMeta, ...frontmatter };
@@ -1387,6 +1448,8 @@ function resolveDocumentConfig(frontmatter = {}, userConfig = {}) {
1387
1448
  const toc = typeof mergedMeta.toc === "boolean" ? mergedMeta.toc : typeof userConfig.toc === "boolean" ? userConfig.toc : DEFAULT_CONFIG.toc;
1388
1449
  const rawWatermark = mergedMeta.watermark !== void 0 ? mergedMeta.watermark : userConfig.watermark !== void 0 ? userConfig.watermark : DEFAULT_CONFIG.watermark;
1389
1450
  const watermark = normalizeWatermark(rawWatermark);
1451
+ const rawSignatures = mergedMeta.signatures || userConfig.signatures;
1452
+ const signatures = normalizeSignatures(rawSignatures, tokenContext);
1390
1453
  const cssList = [];
1391
1454
  const addCss = (item) => {
1392
1455
  if (!item) return;
@@ -1414,6 +1477,7 @@ function resolveDocumentConfig(frontmatter = {}, userConfig = {}) {
1414
1477
  header,
1415
1478
  footer,
1416
1479
  toc,
1480
+ signatures,
1417
1481
  watermark,
1418
1482
  css: cssList,
1419
1483
  embedImages,
@@ -1636,26 +1700,160 @@ ${escapeHtml(node.text || "")}
1636
1700
  watermarkCss = `
1637
1701
  .document-watermark {
1638
1702
  position: fixed;
1639
- top: 50%;
1640
- left: 50%;
1641
- transform: translate(-50%, -50%) rotate(${wm.rotate}deg);
1642
- font-size: ${wm.fontSize}pt;
1643
- font-weight: 900;
1644
- color: ${wm.color};
1645
- opacity: ${wm.opacity};
1703
+ top: 0;
1704
+ left: 0;
1705
+ width: 100vw;
1706
+ height: 100vh;
1646
1707
  pointer-events: none;
1647
1708
  z-index: 0;
1648
1709
  user-select: none;
1649
- text-transform: uppercase;
1650
- letter-spacing: 0.15em;
1651
- white-space: nowrap;
1710
+ -webkit-user-select: none;
1652
1711
  }
1653
1712
  .document-container {
1654
1713
  position: relative;
1655
1714
  z-index: 1;
1656
1715
  }
1716
+ @media print {
1717
+ .document-watermark {
1718
+ position: fixed;
1719
+ top: 0;
1720
+ left: 0;
1721
+ width: 100vw;
1722
+ height: 100vh;
1723
+ -webkit-print-color-adjust: exact;
1724
+ print-color-adjust: exact;
1725
+ }
1726
+ }
1727
+ `;
1728
+ watermarkHtml = ` <div id="markforge-watermark" class="document-watermark" aria-hidden="true"></div>
1729
+ <script>
1730
+ (function() {
1731
+ try {
1732
+ var canvas = document.createElement('canvas');
1733
+ var dpr = 3;
1734
+ var width = 800;
1735
+ var height = 1100;
1736
+ canvas.width = Math.round(width * dpr);
1737
+ canvas.height = Math.round(height * dpr);
1738
+ var ctx = canvas.getContext('2d');
1739
+ if (ctx) {
1740
+ ctx.scale(dpr, dpr);
1741
+ ctx.translate(width / 2, height / 2);
1742
+ ctx.rotate((${wm.rotate} * Math.PI) / 180);
1743
+ ctx.textAlign = 'center';
1744
+ ctx.textBaseline = 'middle';
1745
+ ctx.font = '900 ${wm.fontSize * 1.5}px system-ui, -apple-system, sans-serif';
1746
+ ctx.fillStyle = '${wm.color}';
1747
+ ctx.globalAlpha = ${wm.opacity};
1748
+ try { ctx.letterSpacing = '0.15em'; } catch(e) {}
1749
+ ctx.fillText(${JSON.stringify(wm.text.toUpperCase())}, 0, 0);
1750
+ var dataUrl = canvas.toDataURL('image/png');
1751
+ var wmEl = document.getElementById('markforge-watermark');
1752
+ if (wmEl) {
1753
+ wmEl.style.backgroundImage = 'url("' + dataUrl + '")';
1754
+ wmEl.style.backgroundRepeat = 'no-repeat';
1755
+ wmEl.style.backgroundPosition = 'center center';
1756
+ wmEl.style.backgroundSize = 'contain';
1757
+ }
1758
+ }
1759
+ } catch(err) {}
1760
+ })();
1761
+ </script>
1762
+ `;
1763
+ }
1764
+ let signaturesHtml = "";
1765
+ let signaturesCss = "";
1766
+ if (resolved.signatures && resolved.signatures.items.length > 0) {
1767
+ const sig = resolved.signatures;
1768
+ const numItems = sig.items.length;
1769
+ let justifyCss = "flex-end";
1770
+ if (sig.align === "left") justifyCss = "flex-start";
1771
+ else if (sig.align === "center") justifyCss = "center";
1772
+ else if (sig.align === "space-between") justifyCss = "space-between";
1773
+ signaturesCss = `
1774
+ .markforge-signatures {
1775
+ margin-top: ${sig.spacingBefore};
1776
+ display: grid;
1777
+ grid-template-columns: ${numItems === 1 ? sig.align === "left" ? "minmax(200px, 280px) 1fr" : sig.align === "center" ? "1fr minmax(200px, 280px) 1fr" : "1fr minmax(200px, 280px)" : `repeat(${numItems}, minmax(0, 1fr))`};
1778
+ gap: 2rem;
1779
+ page-break-inside: avoid;
1780
+ break-inside: avoid;
1781
+ }
1782
+ .markforge-signature-card {
1783
+ ${numItems === 1 && sig.align === "center" ? "grid-column: 2;" : ""}
1784
+ ${numItems === 1 && sig.align === "right" ? "grid-column: 2;" : ""}
1785
+ display: flex;
1786
+ flex-direction: column;
1787
+ ${sig.style === "box" ? `border: 1px solid ${sig.borderColor}; border-radius: 6px; padding: 14px 18px; background-color: var(--mf-card-bg, #F8FAFC);` : ""}
1788
+ }
1789
+ .markforge-sig-title {
1790
+ font-size: 0.85rem;
1791
+ color: ${sig.titleColor};
1792
+ font-weight: 600;
1793
+ margin-bottom: 6px;
1794
+ }
1795
+ .markforge-sig-space {
1796
+ height: var(--sig-height, 60px);
1797
+ display: flex;
1798
+ align-items: center;
1799
+ justify-content: center;
1800
+ margin-bottom: 6px;
1801
+ }
1802
+ .markforge-sig-space img {
1803
+ max-height: 100%;
1804
+ max-width: 100%;
1805
+ object-fit: contain;
1806
+ }
1807
+ .markforge-sig-line {
1808
+ ${sig.style === "line" ? `border-bottom: 1.5px solid ${sig.borderColor}; margin-bottom: 8px;` : ""}
1809
+ }
1810
+ .markforge-sig-name {
1811
+ font-size: 0.95rem;
1812
+ font-weight: 700;
1813
+ color: ${sig.nameColor};
1814
+ }
1815
+ .markforge-sig-role {
1816
+ font-size: 0.82rem;
1817
+ color: ${sig.roleColor};
1818
+ margin-top: 2px;
1819
+ }
1820
+ .markforge-sig-date {
1821
+ font-size: 0.78rem;
1822
+ color: ${sig.roleColor};
1823
+ margin-top: 2px;
1824
+ }
1825
+ @media print {
1826
+ .markforge-signatures {
1827
+ page-break-inside: avoid;
1828
+ break-inside: avoid;
1829
+ }
1830
+ }
1657
1831
  `;
1658
- watermarkHtml = ` <div class="document-watermark">${escapeHtml(wm.text)}</div>
1832
+ const itemCards = sig.items.map((item) => {
1833
+ const titleHtml = item.title ? `<div class="markforge-sig-title">${escapeHtml(item.title)}</div>` : "";
1834
+ let signSpaceHtml = "";
1835
+ if (item.image) {
1836
+ signSpaceHtml = `<div class="markforge-sig-space" style="--sig-height: ${item.signatureHeight}px;"><img src="${escapeHtml(item.image)}" alt="Signature" /></div>`;
1837
+ } else {
1838
+ signSpaceHtml = `<div class="markforge-sig-space" style="--sig-height: ${item.signatureHeight}px;"></div>`;
1839
+ }
1840
+ const lineHtml = sig.style === "line" ? `<div class="markforge-sig-line"></div>` : "";
1841
+ const nameHtml = `<div class="markforge-sig-name">${escapeHtml(item.name)}</div>`;
1842
+ const roleHtml = item.role ? `<div class="markforge-sig-role">${escapeHtml(item.role)}</div>` : "";
1843
+ const dateHtml = item.date ? `<div class="markforge-sig-date">Date: ${escapeHtml(item.date)}</div>` : "";
1844
+ return ` <div class="markforge-signature-card">
1845
+ ${titleHtml}
1846
+ ${signSpaceHtml}
1847
+ ${lineHtml}
1848
+ ${nameHtml}
1849
+ ${roleHtml}
1850
+ ${dateHtml}
1851
+ </div>`;
1852
+ }).join("\n");
1853
+ signaturesHtml = `
1854
+ <div class="markforge-signatures">
1855
+ ${itemCards}
1856
+ </div>
1659
1857
  `;
1660
1858
  }
1661
1859
  const hasMermaid = doc.nodes.some((n) => n.type === "mermaid");
@@ -1686,11 +1884,12 @@ ${baseThemeCss}
1686
1884
  ${customCss}
1687
1885
  ${inlinedCss}
1688
1886
  ${watermarkCss}
1887
+ ${signaturesCss}
1689
1888
  </style>
1690
1889
  </head>
1691
1890
  <body>
1692
1891
  ${watermarkHtml} <div class="document-container">
1693
- ${bodyHtml} </div>
1892
+ ${bodyHtml}${signaturesHtml} </div>
1694
1893
  ${mermaidScript}
1695
1894
  </body>
1696
1895
  </html>`;
@@ -2778,6 +2977,59 @@ async function buildDocxDocument(doc, config, baseDir = process.cwd()) {
2778
2977
  continue;
2779
2978
  }
2780
2979
  }
2980
+ if (resolved.signatures && resolved.signatures.items.length > 0) {
2981
+ const sig = resolved.signatures;
2982
+ const numItems = sig.items.length;
2983
+ const contentWidth = Math.max(
2984
+ 1e3,
2985
+ resolved.paperDimensions.widthTwip - resolved.margins.leftTwip - resolved.margins.rightTwip
2986
+ );
2987
+ docElements.push(new Paragraph({ spacing: { before: sig.spacingBeforeTwip } }));
2988
+ const sigCells = [];
2989
+ const colWidths = [];
2990
+ if (numItems === 1) {
2991
+ const cardWidth = Math.min(3400, Math.floor(contentWidth * 0.42));
2992
+ const spacerWidth = contentWidth - cardWidth;
2993
+ const cardCell = await buildDocxSignatureCell(sig.items[0], sig, cardWidth, defaultFont, baseDir);
2994
+ if (sig.align === "left") {
2995
+ colWidths.push(cardWidth, spacerWidth);
2996
+ sigCells.push(cardCell, createEmptyDocxCell(spacerWidth));
2997
+ } else if (sig.align === "center") {
2998
+ const sideWidth = Math.floor(spacerWidth / 2);
2999
+ colWidths.push(sideWidth, cardWidth, sideWidth);
3000
+ sigCells.push(createEmptyDocxCell(sideWidth), cardCell, createEmptyDocxCell(sideWidth));
3001
+ } else {
3002
+ colWidths.push(spacerWidth, cardWidth);
3003
+ sigCells.push(createEmptyDocxCell(spacerWidth), cardCell);
3004
+ }
3005
+ } else {
3006
+ const colWidth = Math.floor(contentWidth / numItems);
3007
+ for (let i = 0; i < numItems; i++) {
3008
+ colWidths.push(colWidth);
3009
+ const cell = await buildDocxSignatureCell(sig.items[i], sig, colWidth, defaultFont, baseDir);
3010
+ sigCells.push(cell);
3011
+ }
3012
+ }
3013
+ const sigTable = new Table({
3014
+ width: { size: 100, type: WidthType.PERCENTAGE },
3015
+ columnWidths: colWidths,
3016
+ borders: {
3017
+ top: { style: BorderStyle.NONE, size: 0, color: "auto" },
3018
+ bottom: { style: BorderStyle.NONE, size: 0, color: "auto" },
3019
+ left: { style: BorderStyle.NONE, size: 0, color: "auto" },
3020
+ right: { style: BorderStyle.NONE, size: 0, color: "auto" },
3021
+ insideHorizontal: { style: BorderStyle.NONE, size: 0, color: "auto" },
3022
+ insideVertical: { style: BorderStyle.NONE, size: 0, color: "auto" }
3023
+ },
3024
+ rows: [
3025
+ new TableRow({
3026
+ cantSplit: true,
3027
+ children: sigCells
3028
+ })
3029
+ ]
3030
+ });
3031
+ docElements.push(sigTable);
3032
+ }
2781
3033
  const contentWidthTwip = Math.max(
2782
3034
  1e3,
2783
3035
  resolved.paperDimensions.widthTwip - resolved.margins.leftTwip - resolved.margins.rightTwip
@@ -3008,6 +3260,140 @@ async function buildDocxDocument(doc, config, baseDir = process.cwd()) {
3008
3260
  });
3009
3261
  return await Packer.toBuffer(document);
3010
3262
  }
3263
+ async function buildDocxSignatureCell(item, sig, widthDxa, defaultFont, baseDir) {
3264
+ const cellParagraphs = [];
3265
+ if (item.title) {
3266
+ cellParagraphs.push(
3267
+ new Paragraph({
3268
+ children: [
3269
+ new TextRun({
3270
+ text: item.title,
3271
+ font: defaultFont,
3272
+ size: 18,
3273
+ // 9pt
3274
+ color: sig.titleColor.replace("#", ""),
3275
+ bold: true
3276
+ })
3277
+ ],
3278
+ spacing: { after: 60 }
3279
+ })
3280
+ );
3281
+ }
3282
+ if (item.image) {
3283
+ const resolvedImg = await resolveImage(item.image, baseDir);
3284
+ if (resolvedImg) {
3285
+ cellParagraphs.push(
3286
+ new Paragraph({
3287
+ children: [
3288
+ new ImageRun({
3289
+ data: resolvedImg.buffer,
3290
+ transformation: {
3291
+ width: 140,
3292
+ height: 60
3293
+ },
3294
+ type: "png"
3295
+ })
3296
+ ],
3297
+ spacing: { before: 40, after: 40 }
3298
+ })
3299
+ );
3300
+ } else {
3301
+ cellParagraphs.push(new Paragraph({ spacing: { before: 240, after: 240 } }));
3302
+ }
3303
+ } else {
3304
+ cellParagraphs.push(new Paragraph({ spacing: { before: 240, after: 240 } }));
3305
+ }
3306
+ if (sig.style === "line") {
3307
+ cellParagraphs.push(
3308
+ new Paragraph({
3309
+ border: {
3310
+ bottom: {
3311
+ style: BorderStyle.SINGLE,
3312
+ size: 6,
3313
+ space: 2,
3314
+ color: sig.borderColor.replace("#", "")
3315
+ }
3316
+ },
3317
+ spacing: { after: 60 }
3318
+ })
3319
+ );
3320
+ }
3321
+ cellParagraphs.push(
3322
+ new Paragraph({
3323
+ children: [
3324
+ new TextRun({
3325
+ text: item.name,
3326
+ font: defaultFont,
3327
+ size: 21,
3328
+ // 10.5pt
3329
+ bold: true,
3330
+ color: sig.nameColor.replace("#", "")
3331
+ })
3332
+ ],
3333
+ spacing: { before: sig.style === "line" ? 40 : 20, after: 20 }
3334
+ })
3335
+ );
3336
+ if (item.role) {
3337
+ cellParagraphs.push(
3338
+ new Paragraph({
3339
+ children: [
3340
+ new TextRun({
3341
+ text: item.role,
3342
+ font: defaultFont,
3343
+ size: 18,
3344
+ // 9pt
3345
+ color: sig.roleColor.replace("#", "")
3346
+ })
3347
+ ],
3348
+ spacing: { after: 20 }
3349
+ })
3350
+ );
3351
+ }
3352
+ if (item.date) {
3353
+ cellParagraphs.push(
3354
+ new Paragraph({
3355
+ children: [
3356
+ new TextRun({
3357
+ text: `Date: ${item.date}`,
3358
+ font: defaultFont,
3359
+ size: 17,
3360
+ // 8.5pt
3361
+ color: sig.roleColor.replace("#", "")
3362
+ })
3363
+ ],
3364
+ spacing: { after: 20 }
3365
+ })
3366
+ );
3367
+ }
3368
+ const isBox = sig.style === "box";
3369
+ const boxBorder = { style: BorderStyle.SINGLE, size: 4, color: sig.borderColor.replace("#", "") };
3370
+ const noneBorder = { style: BorderStyle.NONE, size: 0, color: "auto" };
3371
+ return new TableCell({
3372
+ width: { size: widthDxa, type: WidthType.DXA },
3373
+ shading: isBox ? { fill: "F8FAFC", type: ShadingType.CLEAR } : void 0,
3374
+ margins: isBox ? { top: 140, bottom: 140, left: 160, right: 160 } : { top: 60, bottom: 60, left: 60, right: 60 },
3375
+ borders: {
3376
+ top: isBox ? boxBorder : noneBorder,
3377
+ bottom: isBox ? boxBorder : noneBorder,
3378
+ left: isBox ? boxBorder : noneBorder,
3379
+ right: isBox ? boxBorder : noneBorder
3380
+ },
3381
+ children: cellParagraphs
3382
+ });
3383
+ }
3384
+ function createEmptyDocxCell(widthDxa) {
3385
+ const noneBorder = { style: BorderStyle.NONE, size: 0, color: "auto" };
3386
+ return new TableCell({
3387
+ width: { size: widthDxa, type: WidthType.DXA },
3388
+ borders: {
3389
+ top: noneBorder,
3390
+ bottom: noneBorder,
3391
+ left: noneBorder,
3392
+ right: noneBorder
3393
+ },
3394
+ children: [new Paragraph({})]
3395
+ });
3396
+ }
3011
3397
 
3012
3398
  // src/core/engine.ts
3013
3399
  function formatServerTimestamp(date = /* @__PURE__ */ new Date()) {
@@ -3130,7 +3516,7 @@ try {
3130
3516
  }
3131
3517
  } catch {
3132
3518
  }
3133
- var FALLBACK_VERSION = "0.2.2";
3519
+ var FALLBACK_VERSION = "0.3.0";
3134
3520
  function readVersionFromPackageJson(fromDir) {
3135
3521
  let currentDir = fromDir;
3136
3522
  for (let i = 0; i < 6; i++) {
@@ -3196,6 +3582,7 @@ export {
3196
3582
  compileMarkdown as markforge,
3197
3583
  normalizeHeaderFooter,
3198
3584
  normalizeHeaderFooterSlot,
3585
+ normalizeSignatures,
3199
3586
  normalizeWatermark,
3200
3587
  parseInlineSpans,
3201
3588
  parseMarginToTwip2 as parseMarginToTwip,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@masumdev/markforge",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
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",