diff --git a/src/main.js b/src/main.js
index 1acac86..cb028a0 100644
--- a/src/main.js
+++ b/src/main.js
@@ -9,6 +9,7 @@ const GitOperations = require('./main/GitOperations');
const PdfFontHeader = require('./main/PdfFontHeader');
const MonospaceFontConfig = require('./main/MonospaceFontConfig');
const ExportCss = require('./main/ExportCss');
+const EpubFontEmbedder = require('./main/EpubFontEmbedder');
// Add MiKTeX to PATH for LaTeX support
if (process.platform === 'win32') {
@@ -2838,6 +2839,45 @@ function performExportWithOptions(format, options) {
} else if (format === 'confluence' || format === 'jira') {
pandocCmd = `${getPandocPath()} "${currentFile}" -t jira -o "${outputFile}"`;
exportWithPandoc(pandocCmd, outputFile, format);
+ } else if (format === 'epub') {
+ // Embed the active monospace TTF into EPUB so code blocks render in
+ // JetBrains Mono / Fira Code regardless of the reader's installed fonts.
+ if (pandocSupportsEpubEmbedFont()) {
+ const familyKey = readSettingsJsonCached().monospaceFont || 'jetbrains-mono';
+ const regular = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 400);
+ const bold = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 700);
+ [regular, bold].filter(Boolean).forEach((p) => {
+ pandocCmd += ` --epub-embed-font="${p}"`;
+ });
+ }
+ runPandocCmd(pandocCmd, async (error) => {
+ if (error) {
+ dialog.showErrorBox(
+ 'Export Error',
+ sanitizeErrorMessage(`Failed to export EPUB: ${error.message}`)
+ );
+ return;
+ }
+ // Patch the OPF manifest so readers can locate the embedded font.
+ // Skip silently if the file is missing or pandoc didn't embed.
+ try {
+ const familyKey = readSettingsJsonCached().monospaceFont || 'jetbrains-mono';
+ const regular = MonospaceFontConfig.getMonoFontTtfPath(familyKey, 400);
+ const fonts = [];
+ if (regular) fonts.push({ path: regular, family: familyKey === 'fira-code' ? 'Fira Code' : 'JetBrains Mono', weight: 400 });
+ if (fonts.length) {
+ const patched = await EpubFontEmbedder.patchManifest(outputFile, fonts);
+ try {
+ fs.renameSync(patched, outputFile);
+ } catch (renameErr) {
+ if (typeof console !== 'undefined') console.warn('[epub] could not overwrite with patched EPUB:', renameErr.message);
+ }
+ }
+ } catch (patchErr) {
+ if (typeof console !== 'undefined') console.warn('[epub] manifest patch failed:', patchErr.message);
+ }
+ showExportSuccess(outputFile);
+ });
} else if (format === 'mobi') {
// First export to EPUB, then try ebook-convert if available
const epubFile = outputFile.replace(/\.mobi$/i, '.epub');
diff --git a/src/main/EpubFontEmbedder.js b/src/main/EpubFontEmbedder.js
new file mode 100644
index 0000000..8895420
--- /dev/null
+++ b/src/main/EpubFontEmbedder.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const JSZip = require('jszip');
+
+// Patches an EPUB produced by pandoc so it embeds the supplied TTF font files
+// inside the OEBPS and registers them in the OPF manifest. Used after
+// `pandoc --epub-embed-font=...` runs (which embeds the font data) but does
+// not always add the manifest item we need for readers to discover the font.
+//
+// Writes a new file at `${epubPath}.patched.epub` and returns the patched path.
+// Caller should overwrite the original after a successful export.
+async function patchManifest(epubPath, fonts) {
+ const zip = await JSZip.loadAsync(fs.readFileSync(epubPath));
+ const opfPath = Object.keys(zip.files).find((f) => f.endsWith('content.opf'));
+ if (!opfPath) throw new Error('EPUB has no content.opf');
+ let opf = await zip.file(opfPath).async('string');
+
+ for (const { path: fontPath, family, weight } of fonts) {
+ const filename = path.basename(fontPath);
+ const inFontDir = `OEBPS/fonts/${filename}`;
+ zip.file(inFontDir, fs.readFileSync(fontPath));
+ if (!opf.includes(filename)) {
+ const safeFamily = String(family || 'Font').replace(/\s+/g, '-');
+ const item = ` `;
+ if (opf.includes('')) {
+ opf = opf.replace('', `${item}`);
+ } else {
+ // OPF without a manifest element (unusual but tolerated): inject one
+ // just before so the font item is still discoverable.
+ opf = opf.replace('', `${item}`);
+ }
+ }
+ }
+
+ zip.file(opfPath, opf);
+ const buf = await zip.generateAsync({ type: 'nodebuffer' });
+ const tmp = `${epubPath}.patched.epub`;
+ fs.writeFileSync(tmp, buf);
+ return tmp;
+}
+
+module.exports = { patchManifest };
\ No newline at end of file
diff --git a/tests/epub-font-embedder.test.js b/tests/epub-font-embedder.test.js
new file mode 100644
index 0000000..b02d078
--- /dev/null
+++ b/tests/epub-font-embedder.test.js
@@ -0,0 +1,31 @@
+const fs = require('fs');
+const path = require('path');
+const JSZip = require('jszip');
+const EpubFontEmbedder = require('../src/main/EpubFontEmbedder');
+
+describe('EpubFontEmbedder.patchManifest', () => {
+ const fixturesDir = path.join(__dirname, 'fixtures');
+ const epubPath = path.join(fixturesDir, 'fake.epub');
+ const fontPath = path.join(fixturesDir, 'fake.ttf');
+
+ beforeAll(async () => {
+ fs.mkdirSync(fixturesDir, { recursive: true });
+ fs.writeFileSync(fontPath, 'fake-ttf-binary');
+ const zip = new JSZip();
+ zip.file('OEBPS/content.opf', '');
+ fs.writeFileSync(epubPath, await zip.generateAsync({ type: 'nodebuffer' }));
+ });
+
+ afterAll(() => fs.rmSync(fixturesDir, { recursive: true, force: true }));
+
+ test('adds a manifest entry referencing the TTF when missing', async () => {
+ const patched = await EpubFontEmbedder.patchManifest(epubPath, [
+ { path: fontPath, family: 'JetBrains Mono', weight: 400 },
+ ]);
+ const out = fs.readFileSync(patched);
+ const zip = await JSZip.loadAsync(out);
+ const opf = await zip.file('OEBPS/content.opf').async('string');
+ expect(opf).toMatch(/- ]*href="OEBPS\/fonts\/fake\.ttf"/);
+ expect(opf).toMatch(/
- ]*media-type="application\/x-font-ttf"/);
+ });
+});
\ No newline at end of file