Skip to content
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,30 @@
## Changelog

### v2.3.0 (unreleased) — Breaking API change

**Breaking:** Removed `latinModernFontWithSize:`, `xitsFontWithSize:`, and
`termesFontWithSize:` from `MTFontManager`. Migrate to the generic accessor
with the new public name constants:

```objc
// Before
label.font = [[MTFontManager fontManager] termesFontWithSize:20];

// After
label.font = [[MTFontManager fontManager] fontWithName:MTFontNameTermes size:20];
```

`defaultFont` is unchanged and returns Latin Modern Math at 20pt.

**New fonts:** Added five new OpenType MATH fonts — New Computer Modern Math,
TeX Gyre Pagella Math, STIX Two Math, Fira Math, and Noto Sans Math. XITS Math
updated to v1.302 (final upstream release).

**New public constants** (`MTFontManager.h`): `MTFontNameLatinModern`,
`MTFontNameXITS`, `MTFontNameTermes`, `MTFontNameNewComputerModern`,
`MTFontNamePagella`, `MTFontNameSTIXTwo`, `MTFontNameFiraMath`,
`MTFontNameNotoSansMath`.

### v2.2.0 (2026-05-16)
* Add `\text{}`, `\textrm{}`, `\textbf{}`, `\textit{}`, `\textsf{}`, `\texttt{}` for rendering non-Latin text alongside math — supports CJK, Devanagari, Arabic, Hebrew, Cyrillic, and any other script handled by CoreText system-font cascade.
* Add prime shorthand: `f'` parses as `f^{\prime}`, `f''` as `f^{\prime\prime}`, etc.
Expand Down
27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,22 +200,41 @@ label.fontSize = 30

### Font

The default font is *Latin Modern Math*. Three fonts are bundled; you can
also use any OTF math font:
The default font is *Latin Modern Math*. Eight fonts are bundled; you can
also use any OTF math font. Select a font using `MTFontName*` constants and
`font(withName:size:)`:

```swift
label.font = MTFontManager().termesFont(withSize: 20)
label.font = MTFontManager.fontManager.font(withName: MTFontNameTermes, size: 20)
```

<details>
<summary>Objective-C</summary>

```objective-c
label.font = [[MTFontManager fontManager] termesFontWithSize:20];
label.font = [MTFontManager.fontManager fontWithName:MTFontNameTermes size:20];
```

</details>

The three per-font convenience methods (`latinModernFontWithSize:`,
`xitsFontWithSize:`, `termesFontWithSize:`) were removed. Use
`fontWithName:size:` with one of the bundled constants instead.
`defaultFont` is unchanged and returns Latin Modern Math at 20pt.

Available `MTFontName*` constants:

| Constant | Font |
|---|---|
| `MTFontNameLatinModern` | Latin Modern Math |
| `MTFontNameXITS` | XITS Math |
| `MTFontNameTermes` | TeX Gyre Termes Math |
| `MTFontNameNewComputerModern` | New Computer Modern Math |
| `MTFontNamePagella` | TeX Gyre Pagella Math |
| `MTFontNameSTIXTwo` | STIX Two Math |
| `MTFontNameFiraMath` | Fira Math |
| `MTFontNameNotoSansMath` | Noto Sans Math |

### Color

```swift
Expand Down
15 changes: 9 additions & 6 deletions SwiftMathExample/MathLabel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,22 +38,25 @@ struct MathLabel: View {
}
}

/// The three math fonts bundled with iosMath, exposed for the font switcher.
/// The math fonts bundled with iosMath, exposed for the font switcher.
enum MathFont: String, CaseIterable, Identifiable {
case latinModern = "Latin Modern"
case termes = "TeX Gyre Termes"
case xits = "XITS"

var id: String { rawValue }

func font(size: CGFloat) -> MTFont? {
let manager = MTFontManager.fontManager()
var fontName: String {
switch self {
case .latinModern: return manager.latinModernFont(withSize: size)
case .termes: return manager.termesFont(withSize: size)
case .xits: return manager.xitsFont(withSize: size)
case .latinModern: return MTFontNameLatinModern
case .termes: return MTFontNameTermes
case .xits: return MTFontNameXITS
}
}

func font(size: CGFloat) -> MTFont? {
MTFontManager.fontManager.font(withName: fontName, size: size)
}
Comment on lines +57 to +59
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using MTFontManager() creates a new instance of MTFontManager every time font(size:) is called. This bypasses the singleton instance and defeats the caching mechanism of nameToFontMap, leading to redundant font loading and parsing overhead. Use the shared fontManager() singleton instead.

Suggested change
func font(size: CGFloat) -> MTFont? {
MTFontManager().font(withName: fontName, size: size)
}
func font(size: CGFloat) -> MTFont? {
MTFontManager.fontManager().font(withName: fontName, size: size)
}
References
  1. Use the shared singleton instance MTFontManager.fontManager() instead of creating a new MTFontManager instance to leverage the built-in caching mechanism and avoid performance degradation from re-parsing font files.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 73e3fc4. Switched to MTFontManager.fontManager() so the shared nameToFontMap cache is reused instead of re-parsing the font on every font(size:) call.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correction to the previous reply: MTFontManager.fontManager() does not compile in Swift. Because the Obj-C + (instancetype) fontManager is a factory method whose name matches the class, Swift's Clang importer collapses it into init() and marks fontManager() as unavailable ('fontManager()' has been replaced by 'init()'). So MTFontManager() was actually the only spelling Swift offered — and it resolved to -init, producing a fresh, cache-bypassing instance. The original report was correct on both counts.

Properly fixed in 1956418: fontManager is now a class property (@property (class, readonly, strong) MTFontManager *fontManager;) so Swift imports it as MTFontManager.fontManager (no parens, no init() collapse), and init/new are NS_UNAVAILABLE so the bypass can't be written at all. This call site now uses MTFontManager.fontManager.font(withName:size:).

Verified: swift test (265 tests), the iOS Obj-C example, and the SwiftMathExample app all build.

}

// MARK: - Platform representables
Expand Down
32 changes: 21 additions & 11 deletions iosMath/render/MTFontManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,31 @@

NS_ASSUME_NONNULL_BEGIN

extern NSString *const MTFontNameLatinModern;
extern NSString *const MTFontNameXITS;
extern NSString *const MTFontNameTermes;
extern NSString *const MTFontNameNewComputerModern;
extern NSString *const MTFontNamePagella;
extern NSString *const MTFontNameSTIXTwo;
extern NSString *const MTFontNameFiraMath;
extern NSString *const MTFontNameNotoSansMath;

/** A manager to load font files from disc and keep them
in memory. */
@interface MTFontManager : NSObject

/** Get the singleton instance of MTFontManager. */
+ (instancetype) fontManager;
/** The shared font manager.

Declared as a class property (not a `+fontManager` factory method) so that
Swift imports it as `MTFontManager.fontManager` instead of collapsing it into
`init()`. In Objective-C it is still reached via `[MTFontManager fontManager]`
or `MTFontManager.fontManager`. */
@property (class, readonly, strong) MTFontManager *fontManager;

/** MTFontManager is a singleton; use +fontManager. Constructing your own
instance bypasses the shared font cache, so init/new are unavailable. */
+ (instancetype) new NS_UNAVAILABLE;
- (instancetype) init NS_UNAVAILABLE;

/** Returns the default font, which is Latin Modern Math with 20pt */
- (MTFont *) defaultFont;
Expand All @@ -36,15 +55,6 @@ NS_ASSUME_NONNULL_BEGIN
*/
- (MTFont *) fontWithName:(NSString *)name size:(CGFloat)size;

/** Helper function to return the Xits Math font. */
- (MTFont *) xitsFontWithSize:(CGFloat)size;

/** Helper function to return the Tex Gyre Termes Math font. */
- (MTFont *) termesFontWithSize:(CGFloat)size;

/** Helper function to return the Latin Modern Math font. */
- (MTFont *) latinModernFontWithSize:(CGFloat)size;

/**
Returns a CoreText font suitable for `\text*` rendering. The caller owns
the returned reference (CF_RETAINED) and must `CFRelease` it.
Expand Down
39 changes: 18 additions & 21 deletions iosMath/render/MTFontManager.m
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,15 @@

const int kDefaultFontSize = 20;

NSString *const MTFontNameLatinModern = @"latinmodern-math";
NSString *const MTFontNameXITS = @"xits-math";
NSString *const MTFontNameTermes = @"texgyretermes-math";
NSString *const MTFontNameNewComputerModern = @"newcm-math";
NSString *const MTFontNamePagella = @"texgyrepagella-math";
NSString *const MTFontNameSTIXTwo = @"stixtwo-math";
NSString *const MTFontNameFiraMath = @"firamath";
NSString *const MTFontNameNotoSansMath = @"notosansmath";

@interface MTFontManager ()

@property (nonatomic, nonnull) NSMutableDictionary<NSString*, MTFont*>* nameToFontMap;
Expand All @@ -22,16 +31,19 @@ @interface MTFontManager ()

@implementation MTFontManager

+ (instancetype) fontManager
+ (MTFontManager *) fontManager
{
static MTFontManager* manager = nil;
if (manager == nil) {
manager = [MTFontManager new];
}
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[self alloc] initPrivate];
});
return manager;
}

- (instancetype)init
// init/new are NS_UNAVAILABLE so callers can't bypass the singleton; the
// shared instance is built through this private initializer instead.
- (instancetype)initPrivate
{
self = [super init];
if (self) {
Expand All @@ -54,24 +66,9 @@ - (MTFont *)fontWithName:(NSString *)name size:(CGFloat)size
}
}

- (MTFont *)latinModernFontWithSize:(CGFloat)size
{
return [self fontWithName:@"latinmodern-math" size:size];
}

- (MTFont *)xitsFontWithSize:(CGFloat)size
{
return [self fontWithName:@"xits-math" size:size];
}

- (MTFont *)termesFontWithSize:(CGFloat)size
{
return [self fontWithName:@"texgyretermes-math" size:size];
}

- (MTFont *)defaultFont
{
return [self latinModernFontWithSize:kDefaultFontSize];
return [self fontWithName:MTFontNameLatinModern size:kDefaultFontSize];
}

+ (CTFontRef) textCTFontForStyle:(MTTextStyle) style
Expand Down
50 changes: 11 additions & 39 deletions iosMathExample/example/ViewController.m
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ @interface ViewController () <UITextFieldDelegate>
@property (weak, nonatomic) IBOutlet MTMathUILabel *mathLabel;
@property (weak, nonatomic) IBOutlet UITextField *latexField;

- (void)applyFontWithName:(NSString *)name;

@end

@implementation ViewController
Expand Down Expand Up @@ -253,33 +255,13 @@ - (void) setVerticalGap:(CGFloat) gap between:(UIView*) view1 and:(UIView*) view
}

#pragma mark Buttons
- (void)latinButtonPressed:(id)sender
{
for (MTMathUILabel* label in self.demoLabels) {
label.font = [[MTFontManager fontManager] latinModernFontWithSize:label.font.fontSize];
}
for (MTMathUILabel* label in self.labels) {
label.font = [[MTFontManager fontManager] latinModernFontWithSize:label.font.fontSize];
}
}

- (void)termesButtonPressed:(id)sender
{
for (MTMathUILabel* label in self.demoLabels) {
label.font = [[MTFontManager fontManager] termesFontWithSize:label.font.fontSize];
}
for (MTMathUILabel* label in self.labels) {
label.font = [[MTFontManager fontManager] termesFontWithSize:label.font.fontSize];
}
}

- (void)xitsButtonPressed:(id)sender
- (void)applyFontWithName:(NSString *)name
{
for (MTMathUILabel* label in self.demoLabels) {
label.font = [[MTFontManager fontManager] xitsFontWithSize:label.font.fontSize];
label.font = [[MTFontManager fontManager] fontWithName:name size:label.font.fontSize];
}
for (MTMathUILabel* label in self.labels) {
label.font = [[MTFontManager fontManager] xitsFontWithSize:label.font.fontSize];
label.font = [[MTFontManager fontManager] fontWithName:name size:label.font.fontSize];
}
}

Expand Down Expand Up @@ -341,24 +323,14 @@ - (NSString *)pickerView:(UIPickerView *)pickerView titleForRow:(NSInteger)row f

- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
// Display names (self.fontNames) map 1:1 to these loader keys.
// Not static: extern const NSString* values aren't compile-time constants.
NSString *const kFontKeys[] = {
MTFontNameLatinModern, MTFontNameTermes, MTFontNameXITS,
};
self.controller.fontField.text = self.fontNames[row];
[self.controller.fontField resignFirstResponder];
switch (row) {
case 0:
[self.controller latinButtonPressed:nil];
break;

case 1:
[self.controller termesButtonPressed:nil];
break;

case 2:
[self.controller xitsButtonPressed:nil];
break;

default:
break;
}
[self.controller applyFontWithName:kFontKeys[row]];
}

@end
Expand Down
32 changes: 15 additions & 17 deletions iosMathSwiftTests/iosMathSwiftAPITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,29 +39,27 @@ final class MTMathUILabelTests: XCTestCase {
}

final class MTFontManagerTests: XCTestCase {
func testFontManagerInit() {
let mgr = MTFontManager()
XCTAssertNotNil(mgr)
func testFontManagerSingleton() {
// fontManager is a class property, so repeated access returns the same
// shared instance (and there is no MTFontManager() to bypass it).
XCTAssertTrue(MTFontManager.fontManager === MTFontManager.fontManager)
}

func testDefaultFont() {
let font = MTFontManager().defaultFont()
let font = MTFontManager.fontManager.defaultFont()
XCTAssertNotNil(font)
}

func testLatinModernFont() {
let font = MTFontManager().latinModernFont(withSize: 18)
XCTAssertNotNil(font)
}

func testXitsFont() {
let font = MTFontManager().xitsFont(withSize: 16)
XCTAssertNotNil(font)
}

func testTermesFont() {
let font = MTFontManager().termesFont(withSize: 16)
XCTAssertNotNil(font)
func testFontByName() {
let manager = MTFontManager.fontManager
let names = [
MTFontNameLatinModern, MTFontNameXITS, MTFontNameTermes,
MTFontNameNewComputerModern, MTFontNamePagella, MTFontNameSTIXTwo,
MTFontNameFiraMath, MTFontNameNotoSansMath,
]
for name in names {
XCTAssertNotNil(manager.font(withName: name, size: 18), "Font \(name) failed to load")
}
}
}

Expand Down
4 changes: 2 additions & 2 deletions iosMathTests/MTTypesetterTest.m
Original file line number Diff line number Diff line change
Expand Up @@ -2027,7 +2027,7 @@ - (void)testOverrightarrowNarrow
// horizontal glyph assembly.
- (void)testStretchyArrowAssemblyOnlyFont
{
MTFont* xits = [MTFontManager.fontManager xitsFontWithSize:20];
MTFont* xits = [MTFontManager.fontManager fontWithName:MTFontNameXITS size:20];
XCTAssertNotNil(xits);

for (NSString* latex in @[@"\\overrightarrow{x}", @"\\overrightarrow{ABCD}",
Expand Down Expand Up @@ -2057,7 +2057,7 @@ - (void)testStretchyArrowAssemblyOnlyFont
// vertical glyph assembly instead.
- (void)testStretchyVerticalArrowAssemblyOnlyFont
{
MTFont* xits = [MTFontManager.fontManager xitsFontWithSize:20];
MTFont* xits = [MTFontManager.fontManager fontWithName:MTFontNameXITS size:20];
XCTAssertNotNil(xits);

// Tall content (a fraction) forces the boundary delimiter to stretch, exercising
Expand Down
Loading