feat(main): wire monospace embedders into PDF/DOCX/EPUB/HTML exports

- Add getActiveMonospaceContext() helper that reads settings + resolves TTF
- PDF: build fontspec header and pass --include-in-header to xelatex
- DOCX: post-process zip to embed TTF and add fontTable.xml + rels
- EPUB: prepend --epub-embed-font + patch manifest
- HTML: embed woff2 base64 into built-in marked exporter CSS
- Register get-monospace-settings / set-monospace-settings IPC at startup
This commit is contained in:
2026-07-23 09:34:57 +05:30
parent 4cb38cd861
commit d2f3118bfe
+85
View File
@@ -5,6 +5,7 @@ const { CrashWriter } = require('./updater/crash-writer');
const { MigrationRunner } = require('./updater/migration-runner');
const updaterHandlers = require('./ipc/updater-handlers');
const crashHandlers = require('./ipc/crash-handlers');
const monospaceHandlers = require('./ipc/monospace-handlers');
const path = require('path');
const fs = require('fs');
const { execFile } = require('child_process');
@@ -14,6 +15,24 @@ const { validatePath, resolveWritablePath, isPathAccessible } = require('./utils
const fileOps = require('./files');
const menu = require('./menu');
const { createMainWindow } = require('./window');
const MonospaceFontConfig = require('./MonospaceFontConfig');
const { buildPdfFontHeader } = require('./PdfFontHeader');
const { embedDocxFont } = require('./DocxFontEmbedder');
const { withEpubEmbedFontArgs, embedEpubFont } = require('./EpubFontEmbedder');
const { buildExportCss } = require('./ExportCss');
const { safeMonospaceSettings, DEFAULT_SETTINGS: MONO_DEFAULTS } = require('./settings/monospaceSettings');
/**
* Read active monospace settings + TTF path. Returns null TTF if asset missing.
*/
function getActiveMonospaceContext() {
const settings = safeMonospaceSettings({
monospaceFont: store.get('monospaceFont', MONO_DEFAULTS.monospaceFont),
monospaceLigatures: store.get('monospaceLigatures', MONO_DEFAULTS.monospaceLigatures),
});
const ttf = MonospaceFontConfig.getMonoFontTtfPath(settings.monospaceFont, 400);
return { settings, ttf };
}
// Add MiKTeX to PATH for LaTeX support
if (process.platform === 'win32') {
@@ -1577,6 +1596,23 @@ function performExportWithOptions(format, options) {
pandocCmd += ' -V monofont="Consolas"';
pandocCmd += ' --highlight-style=tango';
// When bundled TTF asset is present, replace system Consolas with our
// active monospace font (xelatex fontspec) so ASCII art renders
// identically across machines.
const monoCtx = getActiveMonospaceContext();
if (monoCtx.ttf) {
try {
const { headerPath } = buildPdfFontHeader(
monoCtx.settings,
monoCtx.ttf,
MonospaceFontConfig.getActiveFamily(monoCtx.settings)
);
pandocCmd += ` --include-in-header="${headerPath}"`;
} catch (e) {
console.error('[monospace] PDF header build failed (non-fatal):', e.message);
}
}
// Add header/footer if enabled
if (headerFooterSettings.enabled) {
const filename = currentFile
@@ -1726,6 +1762,12 @@ function performExportWithOptions(format, options) {
});
} else {
// Generic export for other formats
if (format === 'epub') {
const monoCtx = getActiveMonospaceContext();
if (monoCtx.ttf) {
pandocCmd = ` --epub-embed-font="${monoCtx.ttf}"` + pandocCmd;
}
}
exportWithPandoc(pandocCmd, outputFile, format);
}
})
@@ -1866,6 +1908,22 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
}
}
// Embed active monospace TTF into DOCX so code blocks render in the
// bundled font even when the recipient opens the file without JetBrains
// Mono or Fira Code installed.
if (format === 'docx') {
const monoCtx = getActiveMonospaceContext();
if (monoCtx.ttf) {
try {
const family = MonospaceFontConfig.getActiveFamily(monoCtx.settings).replace(/\s+/g, '');
await embedDocxFont(outputFile, outputFile, monoCtx.ttf, family);
console.log(`[monospace] Embedded ${family} TTF into DOCX`);
} catch (e) {
console.error('[monospace] DOCX embed failed (non-fatal):', e.message);
}
}
}
// Add headers/footers to DOCX if enabled
if (format === 'docx' && headerFooterSettings.enabled) {
try {
@@ -1895,6 +1953,20 @@ function exportWithPandoc(pandocCmd, outputFile, format) {
}
}
// Patch EPUB manifest so the font table references the embedded TTF.
if (format === 'epub') {
const monoCtx = getActiveMonospaceContext();
if (monoCtx.ttf) {
try {
const family = MonospaceFontConfig.getActiveFamily(monoCtx.settings);
await embedEpubFont(outputFile, monoCtx.ttf, family);
console.log(`[monospace] Patched EPUB manifest for ${family}`);
} catch (e) {
console.error('[monospace] EPUB manifest patch failed (non-fatal):', e.message);
}
}
}
// ODT header/footer is not supported by Pandoc's ODT output
// Headers/footers are applied only for DOCX format
@@ -2003,6 +2075,18 @@ function exportToHTML(outputFile) {
word-wrap: normal;
}
}
${(() => {
try {
const monoCtx = getActiveMonospaceContext();
const familyName = MonospaceFontConfig.getActiveFamily(monoCtx.settings);
const woff2Name = familyName === 'Fira Code' ? 'FiraCode-Regular.woff2' : 'JetBrainsMono-Regular.woff2';
const woff2Path = path.join(path.dirname(monoCtx.ttf || ''), woff2Name);
if (!fs.existsSync(woff2Path)) return '';
return buildExportCss(monoCtx.settings, { woff2: fs.readFileSync(woff2Path) });
} catch (e) {
return '';
}
})()}
</style>
</head>
<body>
@@ -3172,6 +3256,7 @@ app.whenReady().then(() => {
crash,
getMainWindow: () => mainWindow,
});
monospaceHandlers.register();
// --------------------------------------------------------------------
// Register file ops IPC handlers