From cdc318ebc7523a7aec187b05414b88d90cc77e91 Mon Sep 17 00:00:00 2001 From: Amit Haridas Date: Tue, 30 Jun 2026 23:30:00 +0530 Subject: [PATCH] feat(monospace): add PdfFontHeader builder for xelatex fontspec --- src/main/PdfFontHeader.js | 43 +++++++++++++++++++++++++++ tests/pdf-font-header.test.js | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 src/main/PdfFontHeader.js create mode 100644 tests/pdf-font-header.test.js diff --git a/src/main/PdfFontHeader.js b/src/main/PdfFontHeader.js new file mode 100644 index 0000000..6c6a1f2 --- /dev/null +++ b/src/main/PdfFontHeader.js @@ -0,0 +1,43 @@ +'use strict'; + +// fontspec accepts forward-slash paths on all platforms (TeX normalizes). +// Normalize Windows backslashes so we can reliably build Path/filename. +function toPosix(p) { + return String(p).replace(/\\/g, '/'); +} + +function escape(s) { + // fontspec values: braces are the only TeX-significant chars we might emit + // from a basename. Backslashes are converted to '/' upstream by toPosix(). + return String(s).replace(/[{}]/g, '\\$&'); +} + +function dirOf(p) { + return toPosix(p).replace(/\/[^/]+$/, '') + '/'; +} + +function baseName(p) { + return toPosix(p).split('/').pop(); +} + +// Strip a weight-style suffix from a TTF filename to get the family prefix +// (e.g. JetBrainsMono-Regular.ttf -> JetBrainsMono). Used as the `\setmonofont` +// argument so `*-Regular` / `*-Bold` globs resolve to the right files. +function weightPrefix(p) { + return baseName(p).replace(/-(Regular|Bold|Light|Medium|SemiBold|Italic)\.ttf$/i, ''); +} + +function build({ fontTtfPath, boldTtfPath, ligatures }) { + if (!fontTtfPath) { + return '% Monospace font path unavailable; TeX will use its default monospace.\n'; + } + + const ligValue = ligatures ? 'Ligatures=TeX' : 'Ligatures=NoCommon'; + const prefix = escape(weightPrefix(fontTtfPath)); + + return `\\usepackage{fontspec} +\\setmonofont[Path=${escape(dirOf(fontTtfPath))},Extension=.ttf,UprightFont=*-Regular,BoldFont=*-Bold,${ligValue}]{${prefix}} +`; +} + +module.exports = { build }; \ No newline at end of file diff --git a/tests/pdf-font-header.test.js b/tests/pdf-font-header.test.js new file mode 100644 index 0000000..109e247 --- /dev/null +++ b/tests/pdf-font-header.test.js @@ -0,0 +1,55 @@ +const PdfFontHeader = require('../src/main/PdfFontHeader'); + +describe('PdfFontHeader.build', () => { + test('emits fontspec with Path, family prefix, weight globs, and Ligatures=NoCommon when off', () => { + const tex = PdfFontHeader.build({ + fontTtfPath: '/abs/path/JetBrainsMono-Regular.ttf', + boldTtfPath: '/abs/path/JetBrainsMono-Bold.ttf', + ligatures: false, + }); + expect(tex).toContain('\\usepackage{fontspec}'); + // \setmonofont uses the family prefix (weight suffix stripped) + expect(tex).toMatch(/\\setmonofont\[[^\]]*\]\{JetBrainsMono\}/); + // Path is the directory (no trailing filename) + expect(tex).toContain('Path=/abs/path/'); + // No programming ligatures + expect(tex).toContain('Ligatures=NoCommon'); + // Upright/Bold use the family-prefixed glob + expect(tex).toContain('UprightFont=*-Regular'); + expect(tex).toContain('BoldFont=*-Bold'); + }); + + test('emits Ligatures=TeX when on (preserves --/--- but no programming ligatures)', () => { + const tex = PdfFontHeader.build({ + fontTtfPath: '/abs/JBM-Regular.ttf', + boldTtfPath: '/abs/JBM-Bold.ttf', + ligatures: true, + }); + expect(tex).toContain('Ligatures=TeX'); + expect(tex).not.toContain('Ligatures=NoCommon'); + }); + + test('returns a no-op stub when font path is missing', () => { + const tex = PdfFontHeader.build({ fontTtfPath: null, boldTtfPath: null, ligatures: false }); + expect(tex).toContain('% Monospace font path unavailable'); + }); + + test('normalizes Windows backslashes to forward slashes', () => { + const tex = PdfFontHeader.build({ + fontTtfPath: 'C:\\Users\\foo\\JetBrainsMono-Regular.ttf', + boldTtfPath: 'C:\\Users\\foo\\JetBrainsMono-Bold.ttf', + ligatures: false, + }); + expect(tex).toContain('Path=C:/Users/foo/'); + expect(tex).toMatch(/\{JetBrainsMono\}/); + }); + + test('strips the FiraCode weight suffix to derive the family prefix', () => { + const tex = PdfFontHeader.build({ + fontTtfPath: '/abs/FiraCode-Regular.ttf', + boldTtfPath: '/abs/FiraCode-Bold.ttf', + ligatures: false, + }); + expect(tex).toMatch(/\{FiraCode\}/); + }); +}); \ No newline at end of file