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.
This commit is contained in:
2026-08-23 19:31:33 +05:30
parent cdc318ebc7
commit 269d4ac028
4 changed files with 52 additions and 7 deletions
+45 -5
View File
@@ -1,10 +1,13 @@
const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron'); const { app, BrowserWindow, Menu, dialog, ipcMain, shell } = require('electron');
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const os = require('os');
const { execFile } = require('child_process'); const { execFile } = require('child_process');
const WordTemplateExporter = require('./wordTemplateExporter'); const WordTemplateExporter = require('./wordTemplateExporter');
const PDFOperations = require('./main/PDFOperations'); const PDFOperations = require('./main/PDFOperations');
const GitOperations = require('./main/GitOperations'); const GitOperations = require('./main/GitOperations');
const PdfFontHeader = require('./main/PdfFontHeader');
const MonospaceFontConfig = require('./main/MonospaceFontConfig');
// Add MiKTeX to PATH for LaTeX support // Add MiKTeX to PATH for LaTeX support
if (process.platform === 'win32') { if (process.platform === 'win32') {
@@ -296,9 +299,42 @@ const store = {
} catch {} } catch {}
settings[key] = value; settings[key] = value;
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2)); 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 // Plugin settings IPC handlers
ipcMain.handle('plugin-settings:get', (_event, key) => { ipcMain.handle('plugin-settings:get', (_event, key) => {
return store.get(key); return store.get(key);
@@ -516,6 +552,8 @@ function pandocSupportsEpubEmbedFont() {
const v = getPandocVersion(); const v = getPandocVersion();
return v.major > 2 || (v.major === 2 && v.minor >= 11); return v.major > 2 || (v.major === 2 && v.minor >= 11);
} }
exports.pandocSupportsEpubEmbedFont = pandocSupportsEpubEmbedFont;
exports.getPandocVersion = getPandocVersion;
function createWindow() { function createWindow() {
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
width: 1200, width: 1200,
@@ -2635,8 +2673,9 @@ function performExportWithOptions(format, options) {
pandocCmd += ` --pdf-engine="${pdfEngine}"`; pandocCmd += ` --pdf-engine="${pdfEngine}"`;
if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`; if (options.geometry) pandocCmd += ` -V geometry:"${options.geometry}"`;
// Add monospace font settings for code blocks (ASCII art preservation) // Embed bundled monospace font so ASCII columns align in the PDF.
pandocCmd += ' -V monofont="Consolas"'; const monoHeader = buildMonospaceHeaderFile();
pandocCmd += ` --include-in-header="${monoHeader}"`;
pandocCmd += ' --highlight-style=tango'; pandocCmd += ' --highlight-style=tango';
// Add header/footer if enabled // Add header/footer if enabled
@@ -2683,7 +2722,7 @@ function performExportWithOptions(format, options) {
runPandocCmd(pandocCmd, (error) => { runPandocCmd(pandocCmd, (error) => {
if (error) { if (error) {
// Try fallback engines if the specified one fails // Try fallback engines if the specified one fails
const fallbackEngines = ['pdflatex', 'lualatex']; const fallbackEngines = ['lualatex', 'pdflatex'];
tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error); tryPdfFallback(currentFile, outputFile, fallbackEngines, 0, options, error);
} else { } else {
showExportSuccess(outputFile); showExportSuccess(outputFile);
@@ -2831,8 +2870,9 @@ function tryPdfFallback(inputFile, outputFile, engines, index, options, _lastErr
const engine = engines[index]; const engine = engines[index];
let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`; let pandocCmd = `${getPandocPath()} "${inputFile}" --pdf-engine=${engine} -o "${outputFile}"`;
// Add monospace font settings for code blocks (ASCII art preservation) // Embed bundled monospace font so ASCII columns align in the PDF.
pandocCmd += ' -V monofont="Consolas"'; const monoHeader = buildMonospaceHeaderFile();
pandocCmd += ` --include-in-header="${monoHeader}"`;
pandocCmd += ' --highlight-style=tango'; pandocCmd += ' --highlight-style=tango';
// Add geometry if specified // Add geometry if specified
-1
View File
@@ -2,7 +2,6 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const { app } = require('electron');
const { getActiveMonoFont, isLigaturesEnabled, FAMILY_BY_KEY } = require('./settings/monospaceSettings'); const { getActiveMonoFont, isLigaturesEnabled, FAMILY_BY_KEY } = require('./settings/monospaceSettings');
const WEIGHT_BY_KEY = { 300: 'Light', 400: 'Regular', 500: 'Medium', 600: 'SemiBold', 700: 'Bold' }; const WEIGHT_BY_KEY = { 300: 'Light', 400: 'Regular', 500: 'Medium', 600: 'SemiBold', 700: 'Bold' };
+4
View File
@@ -32,6 +32,10 @@ function build({ fontTtfPath, boldTtfPath, ligatures }) {
return '% Monospace font path unavailable; TeX will use its default monospace.\n'; 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 ligValue = ligatures ? 'Ligatures=TeX' : 'Ligatures=NoCommon';
const prefix = escape(weightPrefix(fontTtfPath)); const prefix = escape(weightPrefix(fontTtfPath));
+3 -1
View File
@@ -25,7 +25,9 @@ function buildFontFaceBlock(familyKey) {
const data = fs.readFileSync(fontPath); const data = fs.readFileSync(fontPath);
const dataUri = `data:font/woff2;base64,${data.toString('base64')}`; 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'); }`; 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 ''; return '';
} }
} }