Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions MacDown 3000.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@
ISSUE313TOOLBARBUILDFILE /* MPToolbarControllerTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ISSUE313TOOLBARFILEREF /* MPToolbarControllerTests.m */; };
ISSUE318STYLERELBUILDFILE /* MPDocumentStyleUpdateTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ISSUE318STYLERELFILEREF /* MPDocumentStyleUpdateTests.m */; };
ISSUE325MJSCRLBUILDFILE /* MPMathJaxScrollTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ISSUE325MJSCRLFILEREF /* MPMathJaxScrollTests.m */; };
MJLOCALFONTSBUILDFILE /* MPMathJaxLocalFontsTests.m in Sources */ = {isa = PBXBuildFile; fileRef = MJLOCALFONTSFILEREF /* MPMathJaxLocalFontsTests.m */; };
ISSUE331MRMDRNDRBUILDFILE /* MPMermaidRenderingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ISSUE331MRMDRNDRFILEREF /* MPMermaidRenderingTests.m */; };
ISSUE332GVIZRNDRBUILDFILE /* MPGraphvizRenderingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ISSUE332GVIZRNDRFILEREF /* MPGraphvizRenderingTests.m */; };
ISSUE341IMGRENDERBUILDFILE /* MPImageRenderingTests.m in Sources */ = {isa = PBXBuildFile; fileRef = ISSUE341IMGRENDERFILEREF /* MPImageRenderingTests.m */; };
Expand Down Expand Up @@ -732,6 +733,7 @@
ISSUE313TOOLBARFILEREF /* MPToolbarControllerTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPToolbarControllerTests.m; sourceTree = "<group>"; };
ISSUE318STYLERELFILEREF /* MPDocumentStyleUpdateTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPDocumentStyleUpdateTests.m; sourceTree = "<group>"; };
ISSUE325MJSCRLFILEREF /* MPMathJaxScrollTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPMathJaxScrollTests.m; sourceTree = "<group>"; };
MJLOCALFONTSFILEREF /* MPMathJaxLocalFontsTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPMathJaxLocalFontsTests.m; sourceTree = "<group>"; };
ISSUE331MRMDRNDRFILEREF /* MPMermaidRenderingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPMermaidRenderingTests.m; sourceTree = "<group>"; };
ISSUE332GVIZRNDRFILEREF /* MPGraphvizRenderingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPGraphvizRenderingTests.m; sourceTree = "<group>"; };
ISSUE341IMGRENDERFILEREF /* MPImageRenderingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MPImageRenderingTests.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1175,6 +1177,7 @@
8552820CEE8D00E83AEE3897 /* MPHTMLResourceURLsTests.m */,
ISSUE341IMGRENDERFILEREF /* MPImageRenderingTests.m */,
ISSUE325MJSCRLFILEREF /* MPMathJaxScrollTests.m */,
MJLOCALFONTSFILEREF /* MPMathJaxLocalFontsTests.m */,
1FCDCA371944F97800B1F966 /* Supporting Files */,
B3BD139B59E787FBB9F38228 /* MPEditorViewSubstitutionTests.m */,
66A67CD2B90882C7F91B6251 /* MPEditorViewPasteboardTests.m */,
Expand Down Expand Up @@ -1929,6 +1932,7 @@
ISSUE278TABLEBUILDFILE /* MPInsertTableTests.m in Sources */,
ISSUE543EXTCHGBUILDFILE /* MPExternalChangeReloadTests.m in Sources */,
ISSUE325MJSCRLBUILDFILE /* MPMathJaxScrollTests.m in Sources */,
MJLOCALFONTSBUILDFILE /* MPMathJaxLocalFontsTests.m in Sources */,
5AD88BF0766B938F9D61A831 /* MPEditorViewSubstitutionTests.m in Sources */,
C185BB0F201C78DF6D946E1F /* MPEditorViewPasteboardTests.m in Sources */,
ISSUE313TOOLBARBUILDFILE /* MPToolbarControllerTests.m in Sources */,
Expand Down
36 changes: 34 additions & 2 deletions MacDown/Code/Document/MPDocument.m
Original file line number Diff line number Diff line change
Expand Up @@ -1792,14 +1792,46 @@ - (BOOL)textViewShouldMoveToLeftEndOfLine:(NSTextView *)textView

- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource
{

// The MathJax loader is served from the bundle, carrying its "?config=…"
// query so MathJax still loads the right combined config.
if ([[request.URL lastPathComponent] isEqualToString:@"MathJax.js"])
{
NSURLComponents *origComps = [NSURLComponents componentsWithURL:[request URL] resolvingAgainstBaseURL:YES];
NSURLComponents *updatedComps = [NSURLComponents componentsWithURL:[[NSBundle mainBundle] URLForResource:@"MathJax" withExtension:@"js" subdirectory:@"MathJax"] resolvingAgainstBaseURL:NO];
[updatedComps setQueryItems:[origComps queryItems]];

request = [NSURLRequest requestWithURL:[updatedComps URL]];
return [NSURLRequest requestWithURL:[updatedComps URL]];
}

// The TeX HTML-CSS web fonts also ship in the bundle. Serve them locally too:
// without the fonts local, the HTML-CSS output jax waits on a render-time
// web-font fetch and, when it times out, falls back to bitmap image fonts
// that pixelate on zoom. Any MathJax CDN resource whose path maps to a file
// present in the bundle is redirected; the rest (config, jax.js, fontdata.js)
// fall through to the CDN unchanged, so this only ever short-circuits a load
// to a file that exists. The "?V=…" query is dropped — a file: URL with a
// query may not resolve, which would fail the load back into the fallback.
NSURL *cdn = [NSURL URLWithString:kMPMathJaxCDN];
NSURL *url = request.URL;
if (cdn.host.length && [url.host isEqualToString:cdn.host])
{
// Directory on the CDN that holds MathJax.js, e.g.
// "/ajax/libs/mathjax/2.7.3/". The trailing slash keeps a sibling
// version directory (…/2.7.30/) from matching this one's prefix.
NSString *prefix = [[cdn.path stringByDeletingLastPathComponent]
stringByAppendingString:@"/"];
if ([url.path hasPrefix:prefix])
{
NSString *subpath = [url.path substringFromIndex:prefix.length];
NSURL *mathJaxDir =
[[NSBundle mainBundle].resourceURL URLByAppendingPathComponent:@"MathJax"];
NSURL *local = [mathJaxDir URLByAppendingPathComponent:subpath];
if (subpath.length &&
[[NSFileManager defaultManager] fileExistsAtPath:local.path])
{
request = [NSURLRequest requestWithURL:local];
}
}
}

return request;
Expand Down
5 changes: 5 additions & 0 deletions MacDown/Code/Document/MPRenderer.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@
@protocol MPRendererDataSource;
@protocol MPRendererDelegate;

// The MathJax loader URL the preview requests. The bundle serves this and the
// TeX fonts locally (see -[MPDocument webView:resource:willSendRequest:…]), so
// MPDocument matches requests against this origin.
extern NSString * const kMPMathJaxCDN;


typedef NS_ENUM(NSUInteger, MPCodeBlockAccessoryType)
{
Expand Down
53 changes: 41 additions & 12 deletions MacDown/Code/Document/MPRenderer.m
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
// of https://github.com/mathjax/MathJax/issues/548. If the fix has been merged
// in to MathJax, then the WebResourceLoadDelegate can be removed from MPDocument
// and MathJax.js can be removed from this project.
static NSString * const kMPMathJaxCDN =
NSString * const kMPMathJaxCDN =
@"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/MathJax.js"
@"?config=TeX-AMS-MML_HTMLorMML";
static NSString * const kMPPrismScriptDirectory = @"Prism/components";
Expand Down Expand Up @@ -486,6 +486,28 @@ NS_INLINE void MPFreeHTMLRenderer(hoedown_renderer *htmlRenderer)
return escaped;
}

/**
* Origin of kMPMathJaxCDN as a CSP source expression, e.g.
* "https://cdnjs.cloudflare.com".
*
* MathJax resolves its configuration, jax and fonts against the script
* element's own src, so every one of them comes from this origin. Deriving the
* source expression from the URL keeps the policy from drifting away from the
* URL actually loaded when the CDN changes.
*/
NS_INLINE NSString *MPMathJaxCDNOrigin(void)
{
NSURLComponents *cdn = [NSURLComponents componentsWithString:kMPMathJaxCDN];
NSCAssert(cdn.scheme.length && cdn.host.length,
@"kMPMathJaxCDN must carry a scheme and host");
if (cdn.port)
{
return [NSString stringWithFormat:@"%@://%@:%@",
cdn.scheme, cdn.host, cdn.port];
}
return [NSString stringWithFormat:@"%@://%@", cdn.scheme, cdn.host];
}

NS_INLINE NSString *MPPreviewContentSecurityPolicy(void)
{
// MathJax 2.x relies on eval/new Function during startup, and bundled
Expand All @@ -496,17 +518,24 @@ NS_INLINE void MPFreeHTMLRenderer(hoedown_renderer *htmlRenderer)
// that source — the <img> container still lays out, but the bits never
// load. MPImageRenderingTests pins this contract; update it deliberately
// if you tighten the policy.
return @"default-src 'none'; "
@"base-uri 'none'; "
@"form-action 'none'; "
@"object-src 'none'; "
@"frame-src 'none'; "
@"img-src data: file: http: https:; "
@"media-src data: file: http: https:; "
@"style-src 'self' 'unsafe-inline' file:; "
@"font-src data: file:; "
@"connect-src http: https:; "
@"script-src 'self' file: https://cdnjs.cloudflare.com 'unsafe-eval'";
//
// font-src carries the MathJax origin for the same reason script-src does:
// the TeX faces are fetched from it, and blocking them makes the HTML-CSS
// output jax stall for its web-font timeout and then fall back to bitmap
// image fonts.
NSString *mathJax = MPMathJaxCDNOrigin();
return [NSString stringWithFormat:
@"default-src 'none'; "
@"base-uri 'none'; "
@"form-action 'none'; "
@"object-src 'none'; "
@"frame-src 'none'; "
@"img-src data: file: http: https:; "
@"media-src data: file: http: https:; "
@"style-src 'self' 'unsafe-inline' file:; "
@"font-src data: file: %@; "
@"connect-src http: https:; "
@"script-src 'self' file: %@ 'unsafe-eval'", mathJax, mathJax];
}

NS_INLINE NSString *MPPreviewHeadTags(NSString *checkboxBridgeToken)
Expand Down
19 changes: 18 additions & 1 deletion MacDown/Resources/MathJax/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,24 @@

MathJax.Hub.Config({
'showProcessingMessages': false,
'messageStyle': 'none'
'messageStyle': 'none',

// The HTML-CSS output jax prefers fonts installed on the machine over
// MathJax's own, and its default availableFonts list names STIX. macOS
// ships STIX 1.x as system-reserved faces that WebKit resolves by name, so
// the preview picks them up and typesets digits and operators in
// STIXGeneral-Regular — a Times-metric face indistinguishable from body
// text. Browsers that do not enumerate those faces, such as Firefox, load
// MathJax's TeX fonts instead and render the same document correctly.
//
// Emptying the list makes the preview do the same. preferredFont is nulled
// alongside it because MathJax tests the preferred font even when it is
// absent from availableFonts, so the list alone would still take the local
// path on a machine carrying MathJax's own TeX fonts.
'HTML-CSS': {
'availableFonts': [],
'preferredFont': null
}
});

if (typeof MathJaxListener !== 'undefined') {
Expand Down
150 changes: 150 additions & 0 deletions MacDownTests/MPMathJaxLocalFontsTests.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
//
// MPMathJaxLocalFontsTests.m
// MacDown 3000
//
// MathJax's HTML-CSS output jax renders in vector web fonts, but when those
// fonts are fetched from the CDN at render time and the fetch times out it
// falls back to bitmap image fonts that pixelate on zoom. The app bundles the
// TeX woff fonts and the resource-load delegate serves them (and the MathJax
// loader) from the bundle. These tests pin that redirect and the bundled set.
//

#import <XCTest/XCTest.h>
#import <WebKit/WebKit.h>
#import "MPDocument.h"

// The WebResourceLoadDelegate method under test (implemented in MPDocument.m).
@interface MPDocument (LocalFontsTesting)
- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier
willSendRequest:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse
fromDataSource:(WebDataSource *)dataSource;
@end

// Must match kMPMathJaxCDN's origin and version directory in MPRenderer.m.
static NSString * const kMPTestCDNBase =
@"https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.3/";


@interface MPMathJaxLocalFontsTests : XCTestCase
@property (strong) MPDocument *document;
@end


@implementation MPMathJaxLocalFontsTests

- (void)setUp
{
[super setUp];
self.document = [[MPDocument alloc] init];
}

- (void)tearDown
{
self.document = nil;
[super tearDown];
}

/// Runs a CDN request (base + suffix) through the real delegate and returns the
/// URL it decides to load.
- (NSURL *)resolvedURLForSuffix:(NSString *)suffix
{
NSURL *url = [NSURL URLWithString:
[kMPTestCDNBase stringByAppendingString:suffix]];
NSURLRequest *req = [NSURLRequest requestWithURL:url];
NSURLRequest *out = [self.document webView:nil resource:nil
willSendRequest:req redirectResponse:nil
fromDataSource:nil];
return out.URL;
}

- (void)testWoffFontRedirectsToBundleWithoutQuery
{
NSURL *result = [self resolvedURLForSuffix:
@"fonts/HTML-CSS/TeX/woff/MathJax_Main-Regular.woff?V=2.7.3"];

XCTAssertTrue(result.isFileURL,
@"A bundled woff font should be served from the bundle");
XCTAssertNil(result.query,
@"The local font URL must carry no query, or file: may not "
@"resolve and the load falls back to image fonts");
XCTAssertTrue([result.path hasSuffix:
@"/MathJax/fonts/HTML-CSS/TeX/woff/MathJax_Main-Regular.woff"],
@"Should map to the bundled font path, got %@", result.path);
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:result.path],
@"The redirected font must actually exist in the bundle");
}

- (void)testUnbundledResourcePassesThroughToCDN
{
// jax.js is not bundled, so the delegate must leave the request on the CDN.
NSURL *result = [self resolvedURLForSuffix:
@"jax/output/HTML-CSS/jax.js?V=2.7.3"];

XCTAssertFalse(result.isFileURL,
@"An un-bundled resource must not be redirected locally");
XCTAssertEqualObjects(result.host, @"cdnjs.cloudflare.com");
XCTAssertTrue([result.path hasSuffix:@"jax/output/HTML-CSS/jax.js"]);
}

- (void)testMathJaxLoaderStillRedirectsWithConfigQuery
{
// Regression guard for the behaviour the generalised redirect replaces.
NSURL *result = [self resolvedURLForSuffix:
@"MathJax.js?config=TeX-AMS-MML_HTMLorMML"];

XCTAssertTrue(result.isFileURL,
@"MathJax.js should be served from the bundle");
XCTAssertTrue([result.path hasSuffix:@"/MathJax/MathJax.js"],
@"Should map to the bundled loader, got %@", result.path);
XCTAssertEqualObjects(result.query, @"config=TeX-AMS-MML_HTMLorMML",
@"MathJax.js must keep its config query");
}

- (void)testFullTeXWoffSetIsBundled
{
// Authoritative list of the MathJax 2.7.3 HTML-CSS TeX woff files, copied
// from the upstream distribution. It is intentionally NOT enumerated from
// the bundle's own contents: a list read back from what was dropped could
// never fail on an incomplete drop. A missing family (e.g. AMS) would let a
// document's glyphs fetch remotely and pixelate on a timeout.
NSArray<NSString *> *expected = @[
@"MathJax_AMS-Regular",
@"MathJax_Caligraphic-Bold",
@"MathJax_Caligraphic-Regular",
@"MathJax_Fraktur-Bold",
@"MathJax_Fraktur-Regular",
@"MathJax_Main-Bold",
@"MathJax_Main-Italic",
@"MathJax_Main-Regular",
@"MathJax_Math-BoldItalic",
@"MathJax_Math-Italic",
@"MathJax_Math-Regular",
@"MathJax_SansSerif-Bold",
@"MathJax_SansSerif-Italic",
@"MathJax_SansSerif-Regular",
@"MathJax_Script-Regular",
@"MathJax_Size1-Regular",
@"MathJax_Size2-Regular",
@"MathJax_Size3-Regular",
@"MathJax_Size4-Regular",
@"MathJax_Typewriter-Regular",
@"MathJax_Vector-Bold",
@"MathJax_Vector-Regular",
];

NSURL *woffDir = [[NSBundle mainBundle].resourceURL
URLByAppendingPathComponent:@"MathJax/fonts/HTML-CSS/TeX/woff"];

for (NSString *name in expected)
{
NSURL *font = [woffDir URLByAppendingPathComponent:
[name stringByAppendingPathExtension:@"woff"]];
XCTAssertTrue([[NSFileManager defaultManager] fileExistsAtPath:font.path],
@"missing bundled TeX font: %@.woff", name);
}
XCTAssertEqual(expected.count, 22u,
@"The 2.7.3 TeX woff set has 22 files");
}

@end
Loading