From 269d4ac028143134b86796db5a0a538017cbb136 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Tue, 30 Jun 2026 23:31:49 +0530 Subject: [PATCH] feat(monospace): wire PDF export to use bundled monospace font Replaces -V monofont=Consolas with a generated xelatex/lualatex header that fontspec-loads the bundled JetBrains Mono or Fira Code TTF. Adds a cached settings reader with proper invalidation on store.set, and reorders fallback engines to prefer lualatex (fontspec-capable) before pdflatex. --- src/main.js | 50 +++++++++++++++++++++++++++++---- src/main/MonospaceFontConfig.js | 1 - src/main/PdfFontHeader.js | 4 +++ src/print-preview.js | 4 ++- 4 files changed, 52 insertions(+), 7 deletions(-) diff --git a/src/main.js b/src/main.js index 88955a5..5af28a6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,10 +1,13 @@ const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); const path = require('path'); const fs = require('fs'); +const os = require('os'); const { execFile } = require('child_process'); const WordTemplateExporter = require('./wordTemplateExporter'); const PDFOperations = require('./main/PDFOperations'); const GitOperations = require('./main/GitOperations'); +const PdfFontHeader = require('./main/PdfFontHeader'); +const MonospaceFontConfig = require('./main/MonospaceFontConfig'); // Add MiKTeX to PATH for LaTeX support if (process.platform === 'win32') { @@ -296,9 +299,42 @@ const store = { } catch {} settings[key] = value; fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); + _cachedSettings = null; }, }; +// Cached read of the on-disk settings.json with monospace defaults applied. +// Invalidated by store.set. +let _cachedSettings = null; +function readSettingsJsonCached() { + if (_cachedSettings) return _cachedSettings; + try { + _cachedSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); + } catch { + _cachedSettings = {}; + } + _cachedSettings.monospaceFont = _cachedSettings.monospaceFont || 'jetbrains-mono'; + _cachedSettings.monospaceLigatures = _cachedSettings.monospaceLigatures === true; + return _cachedSettings; +} + +// Build a monospace font header (.tex) for xelatex/lualatex from current settings. +// Returns null when the bundled font is unavailable — callers should fall back to defaults. +function buildMonospaceHeaderFile() { + const s = readSettingsJsonCached(); + const familyKey = s.monospaceFont || 'jetbrains-mono'; + const monoFontPath = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 400); + const monoBoldPath = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 700); + const tex = PdfFontHeader.build({ + fontTtfPath: monoFontPath, + boldTtfPath: monoBoldPath, + ligatures: !!s.monospaceLigatures, + }); + const headerFile = path.join(os.tmpdir(), `monospace-pdf-${Date.now()}-${process.pid}.tex`); + fs.writeFileSync(headerFile, tex, 'utf-8'); + return headerFile; +} + // Plugin settings IPC handlers ipcMain.handle('plugin-settings:get', (_event, key) => { return store.get(key); @@ -516,6 +552,8 @@ function pandocSupportsEpubEmbedFont() { const v = getPandocVersion(); return v.major > 2 || (v.major === 2 && v.minor >= 11); } +exports.pandocSupportsEpubEmbedFont = pandocSupportsEpubEmbedFont; +exports.getPandocVersion = getPandocVersion; function createWindow() { mainWindow = new BrowserWindow({ width: 1200, @@ -2635,8 +2673,9 @@ function performExportWithOptions(format, options) { pandocCmd += ` --pdf-engine="${pdfEngine}"`; if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; + // Embed bundled monospace font so ASCII columns align in the PDF. + const monoHeader = buildMonospaceHeaderFile(); + pandocCmd += ` --include-in-header="${monoHeader}"`; pandocCmd += ' --highlight-style=tango'; // Add header/footer if enabled @@ -2683,7 +2722,7 @@ function performExportWithOptions(format, options) { runPandocCmd(pandocCmd, (error) => { if (error) { // Try fallback engines if the specified one fails - const fallbackEngines = ['pdflatex', 'lualatex']; + const fallbackEngines = ['lualatex', 'pdflatex']; tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); } else { showExportSuccess(outputFile); @@ -2831,8 +2870,9 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, _lastErr const engine = engines[index]; let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`; - // Add monospace font settings for code blocks (ASCII art preservation) - pandocCmd += ' -V monofont="Consolas"'; + // Embed bundled monospace font so ASCII columns align in the PDF. + const monoHeader = buildMonospaceHeaderFile(); + pandocCmd += ` --include-in-header="${monoHeader}"`; pandocCmd += ' --highlight-style=tango'; // Add geometry if specified diff --git a/src/main/MonospaceFontConfig.js b/src/main/MonospaceFontConfig.js index f92c477..f9fc2ff 100644 --- a/src/main/MonospaceFontConfig.js +++ b/src/main/MonospaceFontConfig.js @@ -2,7 +2,6 @@ const fs = require('fs'); const path = require('path'); -const { app } = require('electron'); const { getActiveMonoFont, isLigaturesEnabled, FAMILY_BY_KEY } = require('./settings/monospaceSettings'); const WEIGHT_BY_KEY = { 300: 'Light', 400: 'Regular', 500: 'Medium', 600: 'SemiBold', 700: 'Bold' }; diff --git a/src/main/PdfFontHeader.js b/src/main/PdfFontHeader.js index 6c6a1f2..dfcd076 100644 --- a/src/main/PdfFontHeader.js +++ b/src/main/PdfFontHeader.js @@ -32,6 +32,10 @@ function build({ fontTtfPath, boldTtfPath, ligatures }) { return '% Monospace font path unavailable; TeX will use its default monospace.\n'; } + // `boldTtfPath` is accepted for API symmetry with future formats; the glob + // below resolves the bold file from the regular file's family prefix. + void boldTtfPath; + const ligValue = ligatures ? 'Ligatures=TeX' : 'Ligatures=NoCommon'; const prefix = escape(weightPrefix(fontTtfPath)); diff --git a/src/print-preview.js b/src/print-preview.js index 6e05dad..6b50da4 100644 --- a/src/print-preview.js +++ b/src/print-preview.js @@ -25,7 +25,9 @@ function buildFontFaceBlock(familyKey) { const data = fs.readFileSync(fontPath); const dataUri = `data:font/woff2;base64,${data.toString('base64')}`; return `@font-face { font-family: '${family}'; font-weight: 400; font-style: normal; src: url('${dataUri}') format('woff2'); }`; - } catch (_) { + } catch (err) { + // Non-fatal: fall back to the system monospace stack declared in styles-modern.css. + if (typeof console !== 'undefined') console.warn('[print-preview] font embed failed:', err.message); return ''; } }