mirror of
https://github.com/MacDownApp/macdown.git
synced 2026-05-17 12:40:37 +00:00
2018 lines
66 KiB
Objective-C
2018 lines
66 KiB
Objective-C
//
|
||
// MPDocument.m
|
||
// MacDown
|
||
//
|
||
// Created by Tzu-ping Chung on 6/06/2014.
|
||
// Copyright (c) 2014 Tzu-ping Chung . All rights reserved.
|
||
//
|
||
|
||
#import "MPDocument.h"
|
||
#import <WebKit/WebKit.h>
|
||
#import <JJPluralForm/JJPluralForm.h>
|
||
#import <hoedown/html.h>
|
||
#import "hoedown_html_patch.h"
|
||
#import "HGMarkdownHighlighter.h"
|
||
#import "MPUtilities.h"
|
||
#import "MPAutosaving.h"
|
||
#import "NSColor+HTML.h"
|
||
#import "NSDocumentController+Document.h"
|
||
#import "NSPasteboard+Types.h"
|
||
#import "NSString+Lookup.h"
|
||
#import "NSTextView+Autocomplete.h"
|
||
#import "DOMNode+Text.h"
|
||
#import "MPPreferences.h"
|
||
#import "MPDocumentSplitView.h"
|
||
#import "MPEditorView.h"
|
||
#import "MPRenderer.h"
|
||
#import "MPPreferencesViewController.h"
|
||
#import "MPEditorPreferencesViewController.h"
|
||
#import "MPExportPanelAccessoryViewController.h"
|
||
#import "MPMathJaxListener.h"
|
||
#import "WebView+WebViewPrivateHeaders.h"
|
||
#import "MPToolbarController.h"
|
||
#import <JavaScriptCore/JavaScriptCore.h>
|
||
|
||
static NSString * const kMPDefaultAutosaveName = @"Untitled";
|
||
|
||
|
||
NS_INLINE NSString *MPEditorPreferenceKeyWithValueKey(NSString *key)
|
||
{
|
||
if (!key.length)
|
||
return @"editor";
|
||
NSString *first = [[key substringToIndex:1] uppercaseString];
|
||
NSString *rest = [key substringFromIndex:1];
|
||
return [NSString stringWithFormat:@"editor%@%@", first, rest];
|
||
}
|
||
|
||
NS_INLINE NSDictionary *MPEditorKeysToObserve()
|
||
{
|
||
static NSDictionary *keys = nil;
|
||
static dispatch_once_t token;
|
||
dispatch_once(&token, ^{
|
||
keys = @{@"automaticDashSubstitutionEnabled": @NO,
|
||
@"automaticDataDetectionEnabled": @NO,
|
||
@"automaticQuoteSubstitutionEnabled": @NO,
|
||
@"automaticSpellingCorrectionEnabled": @NO,
|
||
@"automaticTextReplacementEnabled": @NO,
|
||
@"continuousSpellCheckingEnabled": @NO,
|
||
@"enabledTextCheckingTypes": @(NSTextCheckingAllTypes),
|
||
@"grammarCheckingEnabled": @NO};
|
||
});
|
||
return keys;
|
||
}
|
||
|
||
NS_INLINE NSSet *MPEditorPreferencesToObserve()
|
||
{
|
||
static NSSet *keys = nil;
|
||
static dispatch_once_t token;
|
||
dispatch_once(&token, ^{
|
||
keys = [NSSet setWithObjects:
|
||
@"editorBaseFontInfo", @"extensionFootnotes",
|
||
@"editorHorizontalInset", @"editorVerticalInset",
|
||
@"editorWidthLimited", @"editorMaximumWidth", @"editorLineSpacing",
|
||
@"editorOnRight", @"editorStyleName", @"editorShowWordCount",
|
||
@"editorScrollsPastEnd", nil
|
||
];
|
||
});
|
||
return keys;
|
||
}
|
||
|
||
NS_INLINE NSString *MPRectStringForAutosaveName(NSString *name)
|
||
{
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
NSString *key = [NSString stringWithFormat:@"NSWindow Frame %@", name];
|
||
NSString *rectString = [defaults objectForKey:key];
|
||
return rectString;
|
||
}
|
||
|
||
NS_INLINE NSColor *MPGetWebViewBackgroundColor(WebView *webview)
|
||
{
|
||
DOMDocument *doc = webview.mainFrameDocument;
|
||
DOMNodeList *nodes = [doc getElementsByTagName:@"body"];
|
||
if (!nodes.length)
|
||
return nil;
|
||
|
||
id bodyNode = [nodes item:0];
|
||
DOMCSSStyleDeclaration *style = [doc getComputedStyle:bodyNode
|
||
pseudoElement:nil];
|
||
return [NSColor colorWithHTMLName:[style backgroundColor]];
|
||
}
|
||
|
||
|
||
@implementation NSURL (Convert)
|
||
|
||
- (NSString *)absoluteBaseURLString
|
||
{
|
||
// Remove fragment (#anchor) and query string.
|
||
NSString *base = self.absoluteString;
|
||
base = [base componentsSeparatedByString:@"?"].firstObject;
|
||
base = [base componentsSeparatedByString:@"#"].firstObject;
|
||
return base;
|
||
}
|
||
|
||
@end
|
||
|
||
|
||
@implementation WebView (Shortcut)
|
||
|
||
- (NSScrollView *)enclosingScrollView
|
||
{
|
||
return self.mainFrame.frameView.documentView.enclosingScrollView;
|
||
}
|
||
|
||
@end
|
||
|
||
|
||
@implementation MPPreferences (Hoedown)
|
||
- (int)extensionFlags
|
||
{
|
||
int flags = 0;
|
||
if (self.extensionAutolink)
|
||
flags |= HOEDOWN_EXT_AUTOLINK;
|
||
if (self.extensionFencedCode)
|
||
flags |= HOEDOWN_EXT_FENCED_CODE;
|
||
if (self.extensionFootnotes)
|
||
flags |= HOEDOWN_EXT_FOOTNOTES;
|
||
if (self.extensionHighlight)
|
||
flags |= HOEDOWN_EXT_HIGHLIGHT;
|
||
if (!self.extensionIntraEmphasis)
|
||
flags |= HOEDOWN_EXT_NO_INTRA_EMPHASIS;
|
||
if (self.extensionQuote)
|
||
flags |= HOEDOWN_EXT_QUOTE;
|
||
if (self.extensionStrikethough)
|
||
flags |= HOEDOWN_EXT_STRIKETHROUGH;
|
||
if (self.extensionSuperscript)
|
||
flags |= HOEDOWN_EXT_SUPERSCRIPT;
|
||
if (self.extensionTables)
|
||
flags |= HOEDOWN_EXT_TABLES;
|
||
if (self.extensionUnderline)
|
||
flags |= HOEDOWN_EXT_UNDERLINE;
|
||
if (self.htmlMathJax)
|
||
flags |= HOEDOWN_EXT_MATH;
|
||
if (self.htmlMathJaxInlineDollar)
|
||
flags |= HOEDOWN_EXT_MATH_EXPLICIT;
|
||
return flags;
|
||
}
|
||
|
||
- (int)rendererFlags
|
||
{
|
||
int flags = 0;
|
||
if (self.htmlTaskList)
|
||
flags |= HOEDOWN_HTML_USE_TASK_LIST;
|
||
if (self.htmlLineNumbers)
|
||
flags |= HOEDOWN_HTML_BLOCKCODE_LINE_NUMBERS;
|
||
if (self.htmlHardWrap)
|
||
flags |= HOEDOWN_HTML_HARD_WRAP;
|
||
if (self.htmlCodeBlockAccessory == MPCodeBlockAccessoryCustom)
|
||
flags |= HOEDOWN_HTML_BLOCKCODE_INFORMATION;
|
||
return flags;
|
||
}
|
||
@end
|
||
|
||
|
||
@interface MPDocument ()
|
||
<NSSplitViewDelegate, NSTextViewDelegate,
|
||
#if __MAC_OS_X_VERSION_MAX_ALLOWED >= 101100
|
||
WebEditingDelegate, WebFrameLoadDelegate, WebPolicyDelegate, WebResourceLoadDelegate,
|
||
#endif
|
||
MPAutosaving, MPRendererDataSource, MPRendererDelegate>
|
||
|
||
typedef NS_ENUM(NSUInteger, MPWordCountType) {
|
||
MPWordCountTypeWord,
|
||
MPWordCountTypeCharacter,
|
||
MPWordCountTypeCharacterNoSpaces,
|
||
};
|
||
|
||
@property (weak) IBOutlet NSToolbar *toolbar;
|
||
@property (weak) IBOutlet MPDocumentSplitView *splitView;
|
||
@property (weak) IBOutlet NSView *editorContainer;
|
||
@property (unsafe_unretained) IBOutlet MPEditorView *editor;
|
||
@property (weak) IBOutlet NSLayoutConstraint *editorPaddingBottom;
|
||
@property (weak) IBOutlet WebView *preview;
|
||
@property (weak) IBOutlet NSPopUpButton *wordCountWidget;
|
||
@property (strong) IBOutlet MPToolbarController *toolbarController;
|
||
@property (copy, nonatomic) NSString *autosaveName;
|
||
@property (strong) HGMarkdownHighlighter *highlighter;
|
||
@property (strong) MPRenderer *renderer;
|
||
@property CGFloat previousSplitRatio;
|
||
@property BOOL manualRender;
|
||
@property BOOL copying;
|
||
@property BOOL printing;
|
||
@property BOOL shouldHandleBoundsChange;
|
||
@property BOOL isPreviewReady;
|
||
@property (strong) NSURL *currentBaseUrl;
|
||
@property CGFloat lastPreviewScrollTop;
|
||
@property (nonatomic, readonly) BOOL needsHtml;
|
||
@property (nonatomic) NSUInteger totalWords;
|
||
@property (nonatomic) NSUInteger totalCharacters;
|
||
@property (nonatomic) NSUInteger totalCharactersNoSpaces;
|
||
@property (strong) NSMenuItem *wordsMenuItem;
|
||
@property (strong) NSMenuItem *charMenuItem;
|
||
@property (strong) NSMenuItem *charNoSpacesMenuItem;
|
||
@property (nonatomic) BOOL needsToUnregister;
|
||
@property (nonatomic) BOOL alreadyRenderingInWeb;
|
||
@property (nonatomic) BOOL renderToWebPending;
|
||
@property (strong) NSArray<NSNumber *> *webViewHeaderLocations;
|
||
@property (strong) NSArray<NSNumber *> *editorHeaderLocations;
|
||
@property (nonatomic) BOOL inLiveScroll;
|
||
|
||
// Store file content in initializer until nib is loaded.
|
||
@property (copy) NSString *loadedString;
|
||
|
||
- (void)scaleWebview;
|
||
- (void)syncScrollers;
|
||
-(void) updateHeaderLocations;
|
||
|
||
@end
|
||
|
||
static void (^MPGetPreviewLoadingCompletionHandler(MPDocument *doc))()
|
||
{
|
||
__weak MPDocument *weakObj = doc;
|
||
return ^{
|
||
WebView *webView = weakObj.preview;
|
||
NSWindow *window = webView.window;
|
||
@synchronized(window) {
|
||
if (window.isFlushWindowDisabled)
|
||
[window enableFlushWindow];
|
||
}
|
||
[weakObj scaleWebview];
|
||
if (weakObj.preferences.editorSyncScrolling)
|
||
{
|
||
[weakObj updateHeaderLocations];
|
||
[weakObj syncScrollers];
|
||
}
|
||
else
|
||
{
|
||
NSClipView *contentView = webView.enclosingScrollView.contentView;
|
||
NSRect bounds = contentView.bounds;
|
||
bounds.origin.y = weakObj.lastPreviewScrollTop;
|
||
contentView.bounds = bounds;
|
||
}
|
||
};
|
||
}
|
||
|
||
|
||
@implementation MPDocument
|
||
|
||
#pragma mark - Accessor
|
||
|
||
- (MPPreferences *)preferences
|
||
{
|
||
return [MPPreferences sharedInstance];
|
||
}
|
||
|
||
- (NSString *)markdown
|
||
{
|
||
return self.editor.string;
|
||
}
|
||
|
||
- (void)setMarkdown:(NSString *)markdown
|
||
{
|
||
self.editor.string = markdown;
|
||
}
|
||
|
||
- (NSString *)html
|
||
{
|
||
return self.renderer.currentHtml;
|
||
}
|
||
|
||
- (BOOL)toolbarVisible
|
||
{
|
||
return self.windowForSheet.toolbar.visible;
|
||
}
|
||
|
||
- (BOOL)previewVisible
|
||
{
|
||
return (self.preview.frame.size.width != 0.0);
|
||
}
|
||
|
||
- (BOOL)editorVisible
|
||
{
|
||
return (self.editorContainer.frame.size.width != 0.0);
|
||
}
|
||
|
||
- (BOOL)needsHtml
|
||
{
|
||
if (self.preferences.markdownManualRender)
|
||
return NO;
|
||
return (self.previewVisible || self.preferences.editorShowWordCount);
|
||
}
|
||
|
||
- (void)setTotalWords:(NSUInteger)value
|
||
{
|
||
_totalWords = value;
|
||
NSString *key = NSLocalizedString(@"WORDS_PLURAL_STRING", @"");
|
||
NSInteger rule = kJJPluralFormRule.integerValue;
|
||
self.wordsMenuItem.title =
|
||
[JJPluralForm pluralStringForNumber:value withPluralForms:key
|
||
usingPluralRule:rule localizeNumeral:NO];
|
||
}
|
||
|
||
- (void)setTotalCharacters:(NSUInteger)value
|
||
{
|
||
_totalCharacters = value;
|
||
NSString *key = NSLocalizedString(@"CHARACTERS_PLURAL_STRING", @"");
|
||
NSInteger rule = kJJPluralFormRule.integerValue;
|
||
self.charMenuItem.title =
|
||
[JJPluralForm pluralStringForNumber:value withPluralForms:key
|
||
usingPluralRule:rule localizeNumeral:NO];
|
||
}
|
||
|
||
- (void)setTotalCharactersNoSpaces:(NSUInteger)value
|
||
{
|
||
_totalCharactersNoSpaces = value;
|
||
NSString *key = NSLocalizedString(@"CHARACTERS_NO_SPACES_PLURAL_STRING",
|
||
@"");
|
||
NSInteger rule = kJJPluralFormRule.integerValue;
|
||
self.charNoSpacesMenuItem.title =
|
||
[JJPluralForm pluralStringForNumber:value withPluralForms:key
|
||
usingPluralRule:rule localizeNumeral:NO];
|
||
}
|
||
|
||
- (void)setAutosaveName:(NSString *)autosaveName
|
||
{
|
||
_autosaveName = autosaveName;
|
||
self.splitView.autosaveName = autosaveName;
|
||
}
|
||
|
||
|
||
#pragma mark - Override
|
||
|
||
- (instancetype)init
|
||
{
|
||
self = [super init];
|
||
if (!self)
|
||
return nil;
|
||
|
||
self.isPreviewReady = NO;
|
||
self.shouldHandleBoundsChange = YES;
|
||
self.previousSplitRatio = -1.0;
|
||
|
||
return self;
|
||
}
|
||
|
||
- (NSString *)windowNibName
|
||
{
|
||
return @"MPDocument";
|
||
}
|
||
|
||
- (void)windowControllerDidLoadNib:(NSWindowController *)controller
|
||
{
|
||
[super windowControllerDidLoadNib:controller];
|
||
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
|
||
// All files use their absolute path to keep their window states.
|
||
NSString *autosaveName = kMPDefaultAutosaveName;
|
||
if (self.fileURL)
|
||
autosaveName = self.fileURL.absoluteString;
|
||
controller.window.frameAutosaveName = autosaveName;
|
||
self.autosaveName = autosaveName;
|
||
|
||
// Perform initial resizing manually because for some reason untitled
|
||
// documents do not pick up the autosaved frame automatically in 10.10.
|
||
NSString *rectString = MPRectStringForAutosaveName(autosaveName);
|
||
if (!rectString)
|
||
rectString = MPRectStringForAutosaveName(kMPDefaultAutosaveName);
|
||
if (rectString)
|
||
[controller.window setFrameFromString:rectString];
|
||
|
||
self.highlighter =
|
||
[[HGMarkdownHighlighter alloc] initWithTextView:self.editor
|
||
waitInterval:0.0];
|
||
self.renderer = [[MPRenderer alloc] init];
|
||
self.renderer.dataSource = self;
|
||
self.renderer.delegate = self;
|
||
|
||
for (NSString *key in MPEditorPreferencesToObserve())
|
||
{
|
||
[defaults addObserver:self forKeyPath:key
|
||
options:NSKeyValueObservingOptionNew context:NULL];
|
||
}
|
||
for (NSString *key in MPEditorKeysToObserve())
|
||
{
|
||
[self.editor addObserver:self forKeyPath:key
|
||
options:NSKeyValueObservingOptionNew context:NULL];
|
||
}
|
||
|
||
self.editor.postsFrameChangedNotifications = YES;
|
||
self.preview.frameLoadDelegate = self;
|
||
self.preview.policyDelegate = self;
|
||
self.preview.editingDelegate = self;
|
||
self.preview.resourceLoadDelegate = self;
|
||
|
||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||
[center addObserver:self selector:@selector(editorTextDidChange:)
|
||
name:NSTextDidChangeNotification object:self.editor];
|
||
[center addObserver:self selector:@selector(userDefaultsDidChange:)
|
||
name:NSUserDefaultsDidChangeNotification
|
||
object:[NSUserDefaults standardUserDefaults]];
|
||
[center addObserver:self selector:@selector(editorBoundsDidChange:)
|
||
name:NSViewBoundsDidChangeNotification
|
||
object:self.editor.enclosingScrollView.contentView];
|
||
[center addObserver:self selector:@selector(editorFrameDidChange:)
|
||
name:NSViewFrameDidChangeNotification object:self.editor];
|
||
[center addObserver:self selector:@selector(didRequestEditorReload:)
|
||
name:MPDidRequestEditorSetupNotification object:nil];
|
||
[center addObserver:self selector:@selector(didRequestPreviewReload:)
|
||
name:MPDidRequestPreviewRenderNotification object:nil];
|
||
[center addObserver:self selector:@selector(willStartLiveScroll:)
|
||
name:NSScrollViewWillStartLiveScrollNotification
|
||
object:self.editor.enclosingScrollView];
|
||
[center addObserver:self selector:@selector(didEndLiveScroll:)
|
||
name:NSScrollViewDidEndLiveScrollNotification
|
||
object:self.editor.enclosingScrollView];
|
||
if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber10_9)
|
||
{
|
||
[center addObserver:self selector:@selector(previewDidLiveScroll:)
|
||
name:NSScrollViewDidEndLiveScrollNotification
|
||
object:self.preview.enclosingScrollView];
|
||
}
|
||
|
||
self.needsToUnregister = YES;
|
||
|
||
self.wordsMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:NULL
|
||
keyEquivalent:@""];
|
||
self.charMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:NULL
|
||
keyEquivalent:@""];
|
||
self.charNoSpacesMenuItem = [[NSMenuItem alloc] initWithTitle:@""
|
||
action:NULL
|
||
keyEquivalent:@""];
|
||
|
||
NSPopUpButton *wordCountWidget = self.wordCountWidget;
|
||
[wordCountWidget removeAllItems];
|
||
[wordCountWidget.menu addItem:self.wordsMenuItem];
|
||
[wordCountWidget.menu addItem:self.charMenuItem];
|
||
[wordCountWidget.menu addItem:self.charNoSpacesMenuItem];
|
||
[wordCountWidget selectItemAtIndex:self.preferences.editorWordCountType];
|
||
wordCountWidget.alphaValue = 0.9;
|
||
wordCountWidget.hidden = !self.preferences.editorShowWordCount;
|
||
wordCountWidget.enabled = NO;
|
||
|
||
// These needs to be queued until after the window is shown, so that editor
|
||
// can have the correct dimention for size-limiting and stuff. See
|
||
// https://github.com/uranusjr/macdown/issues/236
|
||
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
|
||
[self setupEditor:nil];
|
||
[self redrawDivider];
|
||
[self reloadFromLoadedString];
|
||
}];
|
||
}
|
||
|
||
- (void)reloadFromLoadedString
|
||
{
|
||
if (self.loadedString && self.editor && self.renderer && self.highlighter)
|
||
{
|
||
self.editor.string = self.loadedString;
|
||
self.loadedString = nil;
|
||
[self.renderer parseAndRenderNow];
|
||
[self.highlighter parseAndHighlightNow];
|
||
}
|
||
}
|
||
|
||
- (void)close
|
||
{
|
||
if (self.needsToUnregister)
|
||
{
|
||
// Close can be called multiple times, but this can only be done once.
|
||
// http://www.cocoabuilder.com/archive/cocoa/240166-nsdocument-close-method-calls-itself.html
|
||
self.needsToUnregister = NO;
|
||
|
||
// Need to cleanup these so that callbacks won't crash the app.
|
||
[self.highlighter deactivate];
|
||
self.highlighter.targetTextView = nil;
|
||
self.highlighter = nil;
|
||
self.renderer = nil;
|
||
self.preview.frameLoadDelegate = nil;
|
||
self.preview.policyDelegate = nil;
|
||
|
||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
|
||
for (NSString *key in MPEditorPreferencesToObserve())
|
||
[defaults removeObserver:self forKeyPath:key];
|
||
for (NSString *key in MPEditorKeysToObserve())
|
||
[self.editor removeObserver:self forKeyPath:key];
|
||
}
|
||
|
||
[super close];
|
||
}
|
||
|
||
+ (BOOL)autosavesInPlace
|
||
{
|
||
return YES;
|
||
}
|
||
|
||
+ (NSArray *)writableTypes
|
||
{
|
||
return @[@"net.daringfireball.markdown"];
|
||
}
|
||
|
||
- (BOOL)isDocumentEdited
|
||
{
|
||
// Prevent save dialog on an unnamed, empty document. The file will still
|
||
// show as modified (because it is), but no save dialog will be presented
|
||
// when the user closes it.
|
||
if (!self.presentedItemURL && !self.editor.string.length)
|
||
return NO;
|
||
return [super isDocumentEdited];
|
||
}
|
||
|
||
- (BOOL)writeToURL:(NSURL *)url ofType:(NSString *)typeName
|
||
error:(NSError *__autoreleasing *)outError
|
||
{
|
||
if (self.preferences.editorEnsuresNewlineAtEndOfFile)
|
||
{
|
||
NSCharacterSet *newline = [NSCharacterSet newlineCharacterSet];
|
||
NSString *text = self.editor.string;
|
||
NSUInteger end = text.length;
|
||
if (end && ![newline characterIsMember:[text characterAtIndex:end - 1]])
|
||
{
|
||
NSRange selection = self.editor.selectedRange;
|
||
[self.editor insertText:@"\n" replacementRange:NSMakeRange(end, 0)];
|
||
self.editor.selectedRange = selection;
|
||
}
|
||
}
|
||
return [super writeToURL:url ofType:typeName error:outError];
|
||
}
|
||
|
||
- (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError
|
||
{
|
||
return [self.editor.string dataUsingEncoding:NSUTF8StringEncoding];
|
||
}
|
||
|
||
- (BOOL)readFromData:(NSData *)data ofType:(NSString *)typeName
|
||
error:(NSError **)outError
|
||
{
|
||
NSString *content = [[NSString alloc] initWithData:data
|
||
encoding:NSUTF8StringEncoding];
|
||
if (!content)
|
||
return NO;
|
||
|
||
self.loadedString = content;
|
||
[self reloadFromLoadedString];
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)prepareSavePanel:(NSSavePanel *)savePanel
|
||
{
|
||
savePanel.extensionHidden = NO;
|
||
if (self.fileURL && self.fileURL.isFileURL)
|
||
{
|
||
NSString *path = self.fileURL.path;
|
||
|
||
// Use path of parent directory if this is a file. Otherwise this is it.
|
||
BOOL isDir = NO;
|
||
BOOL exists = [[NSFileManager defaultManager] fileExistsAtPath:path
|
||
isDirectory:&isDir];
|
||
if (!exists || !isDir)
|
||
path = [path stringByDeletingLastPathComponent];
|
||
|
||
savePanel.directoryURL = [NSURL fileURLWithPath:path];
|
||
}
|
||
else
|
||
{
|
||
// Suggest a file name for new documents.
|
||
NSString *fileName = self.presumedFileName;
|
||
if (fileName && ![fileName hasExtension:@"md"])
|
||
{
|
||
fileName = [fileName stringByAppendingPathExtension:@"md"];
|
||
savePanel.nameFieldStringValue = fileName;
|
||
}
|
||
}
|
||
|
||
// Get supported extensions from plist
|
||
static NSMutableArray *supportedExtensions = nil;
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
supportedExtensions = [NSMutableArray array];
|
||
NSDictionary *infoDict = [NSBundle mainBundle].infoDictionary;
|
||
for (NSDictionary *docType in infoDict[@"CFBundleDocumentTypes"])
|
||
{
|
||
NSArray *exts = docType[@"CFBundleTypeExtensions"];
|
||
if (exts.count)
|
||
{
|
||
[supportedExtensions addObjectsFromArray:exts];
|
||
}
|
||
}
|
||
});
|
||
|
||
savePanel.allowedFileTypes = supportedExtensions;
|
||
savePanel.allowsOtherFileTypes = YES; // Allow all extensions.
|
||
|
||
return [super prepareSavePanel:savePanel];
|
||
}
|
||
|
||
- (NSPrintInfo *)printInfo
|
||
{
|
||
NSPrintInfo *info = [super printInfo];
|
||
if (!info)
|
||
info = [[NSPrintInfo sharedPrintInfo] copy];
|
||
info.horizontalPagination = NSAutoPagination;
|
||
info.verticalPagination = NSAutoPagination;
|
||
info.verticallyCentered = NO;
|
||
info.topMargin = 50.0;
|
||
info.leftMargin = 0.0;
|
||
info.rightMargin = 0.0;
|
||
info.bottomMargin = 50.0;
|
||
return info;
|
||
}
|
||
|
||
- (NSPrintOperation *)printOperationWithSettings:(NSDictionary *)printSettings
|
||
error:(NSError *__autoreleasing *)e
|
||
{
|
||
NSPrintInfo *info = [self.printInfo copy];
|
||
[info.dictionary addEntriesFromDictionary:printSettings];
|
||
|
||
WebFrameView *view = self.preview.mainFrame.frameView;
|
||
NSPrintOperation *op = [view printOperationWithPrintInfo:info];
|
||
return op;
|
||
}
|
||
|
||
- (void)printDocumentWithSettings:(NSDictionary *)printSettings
|
||
showPrintPanel:(BOOL)showPrintPanel delegate:(id)delegate
|
||
didPrintSelector:(SEL)selector contextInfo:(void *)contextInfo
|
||
{
|
||
self.printing = YES;
|
||
NSInvocation *invocation = nil;
|
||
if (delegate && selector)
|
||
{
|
||
NSMethodSignature *signature =
|
||
[NSMethodSignature methodSignatureForSelector:selector];
|
||
invocation = [NSInvocation invocationWithMethodSignature:signature];
|
||
invocation.target = delegate;
|
||
if (contextInfo)
|
||
[invocation setArgument:&contextInfo atIndex:2];
|
||
}
|
||
[super printDocumentWithSettings:printSettings
|
||
showPrintPanel:showPrintPanel delegate:self
|
||
didPrintSelector:@selector(document:didPrint:context:)
|
||
contextInfo:(void *)invocation];
|
||
}
|
||
|
||
- (BOOL)validateUserInterfaceItem:(id<NSValidatedUserInterfaceItem>)item
|
||
{
|
||
BOOL result = [super validateUserInterfaceItem:item];
|
||
SEL action = item.action;
|
||
if (action == @selector(toggleToolbar:))
|
||
{
|
||
NSMenuItem *it = ((NSMenuItem *)item);
|
||
it.title = self.toolbarVisible ?
|
||
NSLocalizedString(@"Hide Toolbar",
|
||
@"Toggle reveal toolbar") :
|
||
NSLocalizedString(@"Show Toolbar",
|
||
@"Toggle reveal toolbar");
|
||
}
|
||
else if (action == @selector(togglePreviewPane:))
|
||
{
|
||
NSMenuItem *it = ((NSMenuItem *)item);
|
||
it.hidden = (!self.previewVisible && self.previousSplitRatio < 0.0);
|
||
it.title = self.previewVisible ?
|
||
NSLocalizedString(@"Hide Preview Pane",
|
||
@"Toggle preview pane menu item") :
|
||
NSLocalizedString(@"Restore Preview Pane",
|
||
@"Toggle preview pane menu item");
|
||
|
||
}
|
||
else if (action == @selector(toggleEditorPane:))
|
||
{
|
||
NSMenuItem *it = (NSMenuItem*)item;
|
||
it.title = self.editorVisible ?
|
||
NSLocalizedString(@"Hide Editor Pane",
|
||
@"Toggle editor pane menu item") :
|
||
NSLocalizedString(@"Restore Editor Pane",
|
||
@"Toggle editor pane menu item");
|
||
}
|
||
return result;
|
||
}
|
||
|
||
|
||
#pragma mark - NSSplitViewDelegate
|
||
|
||
- (void)splitViewDidResizeSubviews:(NSNotification *)notification
|
||
{
|
||
[self redrawDivider];
|
||
self.editor.editable = self.editorVisible;
|
||
}
|
||
|
||
|
||
#pragma mark - NSTextViewDelegate
|
||
|
||
- (BOOL)textView:(NSTextView *)textView doCommandBySelector:(SEL)commandSelector
|
||
{
|
||
if (commandSelector == @selector(insertTab:))
|
||
return ![self textViewShouldInsertTab:textView];
|
||
else if (commandSelector == @selector(insertBacktab:))
|
||
return ![self textViewShouldInsertBacktab:textView];
|
||
else if (commandSelector == @selector(insertNewline:))
|
||
return ![self textViewShouldInsertNewline:textView];
|
||
else if (commandSelector == @selector(deleteBackward:))
|
||
return ![self textViewShouldDeleteBackward:textView];
|
||
else if (commandSelector == @selector(moveToLeftEndOfLine:))
|
||
return ![self textViewShouldMoveToLeftEndOfLine:textView];
|
||
return NO;
|
||
}
|
||
|
||
- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)range
|
||
replacementString:(NSString *)str
|
||
{
|
||
// Ignore if this originates from an IM marked text commit event.
|
||
if (NSIntersectionRange(textView.markedRange, range).length)
|
||
return YES;
|
||
|
||
if (self.preferences.editorCompleteMatchingCharacters)
|
||
{
|
||
BOOL strikethrough = self.preferences.extensionStrikethough;
|
||
if ([textView completeMatchingCharactersForTextInRange:range
|
||
withString:str
|
||
strikethroughEnabled:strikethrough])
|
||
return NO;
|
||
}
|
||
|
||
// For every change, set the typing attributes
|
||
if (range.location > 0) {
|
||
NSRange prevRange = range;
|
||
prevRange.location -= 1;
|
||
prevRange.length = 1;
|
||
|
||
NSDictionary *attr = [[textView attributedString] fontAttributesInRange:prevRange];
|
||
[textView setTypingAttributes:attr];
|
||
}
|
||
|
||
return YES;
|
||
}
|
||
|
||
#pragma mark - Fake NSTextViewDelegate
|
||
|
||
- (BOOL)textViewShouldInsertTab:(NSTextView *)textView
|
||
{
|
||
if (textView.selectedRange.length != 0)
|
||
{
|
||
[self indent:nil];
|
||
return NO;
|
||
}
|
||
else if (self.preferences.editorConvertTabs)
|
||
{
|
||
[textView insertSpacesForTab];
|
||
return NO;
|
||
}
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)textViewShouldInsertBacktab:(NSTextView *)textView
|
||
{
|
||
[self unindent:nil];
|
||
return NO;
|
||
}
|
||
|
||
- (BOOL)textViewShouldInsertNewline:(NSTextView *)textView
|
||
{
|
||
if ([textView insertMappedContent])
|
||
return NO;
|
||
|
||
BOOL inserts = self.preferences.editorInsertPrefixInBlock;
|
||
if (inserts && [textView completeNextListItem:
|
||
self.preferences.editorAutoIncrementNumberedLists])
|
||
return NO;
|
||
if (inserts && [textView completeNextBlockquoteLine])
|
||
return NO;
|
||
if ([textView completeNextIndentedLine])
|
||
return NO;
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)textViewShouldDeleteBackward:(NSTextView *)textView
|
||
{
|
||
NSRange selectedRange = textView.selectedRange;
|
||
if (self.preferences.editorCompleteMatchingCharacters)
|
||
{
|
||
NSUInteger location = selectedRange.location;
|
||
if ([textView deleteMatchingCharactersAround:location])
|
||
return NO;
|
||
}
|
||
if (self.preferences.editorConvertTabs && !selectedRange.length)
|
||
{
|
||
NSUInteger location = selectedRange.location;
|
||
if ([textView unindentForSpacesBefore:location])
|
||
return NO;
|
||
}
|
||
return YES;
|
||
}
|
||
|
||
- (BOOL)textViewShouldMoveToLeftEndOfLine:(NSTextView *)textView
|
||
{
|
||
if (!self.preferences.editorSmartHome)
|
||
return YES;
|
||
NSUInteger cur = textView.selectedRange.location;
|
||
NSUInteger location =
|
||
[textView.string locationOfFirstNonWhitespaceCharacterInLineBefore:cur];
|
||
if (location == cur || cur == 0)
|
||
return YES;
|
||
else if (cur >= textView.string.length)
|
||
cur = textView.string.length - 1;
|
||
|
||
// We don't want to jump rows when the line is wrapped. (#103)
|
||
// If the line is wrapped, the target will be higher than the current glyph.
|
||
NSLayoutManager *manager = textView.layoutManager;
|
||
NSTextContainer *container = textView.textContainer;
|
||
NSRect targetRect =
|
||
[manager boundingRectForGlyphRange:NSMakeRange(location, 1)
|
||
inTextContainer:container];
|
||
NSRect currentRect =
|
||
[manager boundingRectForGlyphRange:NSMakeRange(cur, 1)
|
||
inTextContainer:container];
|
||
if (targetRect.origin.y != currentRect.origin.y)
|
||
return YES;
|
||
|
||
textView.selectedRange = NSMakeRange(location, 0);
|
||
return NO;
|
||
}
|
||
|
||
|
||
#pragma mark - WebResourceLoadDelegate
|
||
|
||
- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource
|
||
{
|
||
|
||
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 request;
|
||
}
|
||
|
||
#pragma mark - WebFrameLoadDelegate
|
||
|
||
- (void)webView:(WebView *)sender didCommitLoadForFrame:(WebFrame *)frame
|
||
{
|
||
NSWindow *window = sender.window;
|
||
@synchronized(window) {
|
||
if (!window.isFlushWindowDisabled)
|
||
[window disableFlushWindow];
|
||
}
|
||
|
||
// If MathJax is off, the on-completion callback will be invoked directly
|
||
// when loading is done (in -webView:didFinishLoadForFrame:).
|
||
if (self.preferences.htmlMathJax)
|
||
{
|
||
MPMathJaxListener *listener = [[MPMathJaxListener alloc] init];
|
||
[listener addCallback:MPGetPreviewLoadingCompletionHandler(self)
|
||
forKey:@"End"];
|
||
[sender.windowScriptObject setValue:listener forKey:@"MathJaxListener"];
|
||
}
|
||
}
|
||
|
||
- (void)webView:(WebView *)sender didFinishLoadForFrame:(WebFrame *)frame
|
||
{
|
||
// If MathJax is on, the on-completion callback will be invoked by the
|
||
// JavaScript handler injected in -webView:didCommitLoadForFrame:.
|
||
if (!self.preferences.htmlMathJax)
|
||
{
|
||
id callback = MPGetPreviewLoadingCompletionHandler(self);
|
||
NSOperationQueue *queue = [NSOperationQueue mainQueue];
|
||
[queue addOperationWithBlock:callback];
|
||
}
|
||
|
||
self.isPreviewReady = YES;
|
||
|
||
// Update word count
|
||
if (self.preferences.editorShowWordCount)
|
||
[self updateWordCount];
|
||
|
||
self.alreadyRenderingInWeb = NO;
|
||
|
||
if (self.renderToWebPending)
|
||
[self.renderer parseAndRenderNow];
|
||
|
||
self.renderToWebPending = NO;
|
||
}
|
||
|
||
- (void)webView:(WebView *)sender didFailLoadWithError:(NSError *)error
|
||
forFrame:(WebFrame *)frame
|
||
{
|
||
[self webView:sender didFinishLoadForFrame:frame];
|
||
|
||
self.alreadyRenderingInWeb = NO;
|
||
|
||
if (self.renderToWebPending)
|
||
[self.renderer parseAndRenderNow];
|
||
|
||
self.renderToWebPending = NO;
|
||
}
|
||
|
||
|
||
#pragma mark - WebPolicyDelegate
|
||
|
||
- (void)webView:(WebView *)webView
|
||
decidePolicyForNavigationAction:(NSDictionary *)information
|
||
request:(NSURLRequest *)request frame:(WebFrame *)frame
|
||
decisionListener:(id<WebPolicyDecisionListener>)listener
|
||
{
|
||
switch ([information[WebActionNavigationTypeKey] integerValue])
|
||
{
|
||
case WebNavigationTypeLinkClicked:
|
||
// If the target is exactly as the current one, ignore.
|
||
if ([self.currentBaseUrl isEqual:request.URL])
|
||
{
|
||
[listener ignore];
|
||
return;
|
||
}
|
||
// If this is a different page, intercept and handle ourselves.
|
||
else if (![self isCurrentBaseUrl:request.URL])
|
||
{
|
||
[listener ignore];
|
||
[self openOrCreateFileForUrl:request.URL];
|
||
return;
|
||
}
|
||
// Otherwise this is somewhere else on the same page. Jump there.
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
[listener use];
|
||
}
|
||
|
||
|
||
#pragma mark - WebEditingDelegate
|
||
|
||
- (BOOL)webView:(WebView *)webView doCommandBySelector:(SEL)selector
|
||
{
|
||
if (selector == @selector(copy:))
|
||
{
|
||
NSString *html = webView.selectedDOMRange.markupString;
|
||
|
||
// Inject the HTML content later so that it doesn't get cleared during
|
||
// the native copy operation.
|
||
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
|
||
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
||
if (![pb stringForType:@"public.html"])
|
||
[pb setString:html forType:@"public.html"];
|
||
}];
|
||
}
|
||
return NO;
|
||
}
|
||
|
||
#pragma mark - WebUIDelegate
|
||
|
||
- (NSUInteger)webView:(WebView *)webView
|
||
dragDestinationActionMaskForDraggingInfo:(id<NSDraggingInfo>)info
|
||
{
|
||
return WebDragDestinationActionNone;
|
||
}
|
||
|
||
#pragma mark - MPRendererDataSource
|
||
|
||
- (BOOL)rendererLoading {
|
||
return self.preview.loading;
|
||
}
|
||
|
||
- (NSString *)rendererMarkdown:(MPRenderer *)renderer
|
||
{
|
||
return self.editor.string;
|
||
}
|
||
|
||
- (NSString *)rendererHTMLTitle:(MPRenderer *)renderer
|
||
{
|
||
NSString *n = self.fileURL.lastPathComponent.stringByDeletingPathExtension;
|
||
return n ? n : @"";
|
||
}
|
||
|
||
|
||
#pragma mark - MPRendererDelegate
|
||
|
||
- (int)rendererExtensions:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.extensionFlags;
|
||
}
|
||
|
||
- (BOOL)rendererHasSmartyPants:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.extensionSmartyPants;
|
||
}
|
||
|
||
- (BOOL)rendererRendersTOC:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlRendersTOC;
|
||
}
|
||
|
||
- (NSString *)rendererStyleName:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlStyleName;
|
||
}
|
||
|
||
- (BOOL)rendererDetectsFrontMatter:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlDetectFrontMatter;
|
||
}
|
||
|
||
- (BOOL)rendererHasSyntaxHighlighting:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlSyntaxHighlighting;
|
||
}
|
||
|
||
- (BOOL)rendererHasMermaid:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlMermaid;
|
||
}
|
||
|
||
- (BOOL)rendererHasGraphviz:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlGraphviz;
|
||
}
|
||
|
||
- (MPCodeBlockAccessoryType)rendererCodeBlockAccesory:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlCodeBlockAccessory;
|
||
}
|
||
|
||
- (BOOL)rendererHasMathJax:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlMathJax;
|
||
}
|
||
|
||
- (NSString *)rendererHighlightingThemeName:(MPRenderer *)renderer
|
||
{
|
||
return self.preferences.htmlHighlightingThemeName;
|
||
}
|
||
|
||
- (void)renderer:(MPRenderer *)renderer didProduceHTMLOutput:(NSString *)html
|
||
{
|
||
if (self.alreadyRenderingInWeb)
|
||
{
|
||
self.renderToWebPending = YES;
|
||
return;
|
||
}
|
||
|
||
if (self.printing)
|
||
return;
|
||
|
||
self.alreadyRenderingInWeb = YES;
|
||
|
||
// Delayed copying for -copyHtml.
|
||
if (self.copying)
|
||
{
|
||
self.copying = NO;
|
||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
||
[pasteboard clearContents];
|
||
[pasteboard writeObjects:@[self.renderer.currentHtml]];
|
||
}
|
||
|
||
NSURL *baseUrl = self.fileURL;
|
||
if (!baseUrl) // Unsaved doument; just use the default URL.
|
||
baseUrl = self.preferences.htmlDefaultDirectoryUrl;
|
||
|
||
self.manualRender = self.preferences.markdownManualRender;
|
||
|
||
#if 0
|
||
// Unfortunately this DOM-replacing causes a lot of problems...
|
||
// 1. MathJax needs to be triggered.
|
||
// 2. Prism rendering is lost.
|
||
// 3. Potentially more.
|
||
// Essentially all JavaScript needs to be run again after we replace
|
||
// the DOM. I have no idea how many more problems there are, so we'll have
|
||
// to back off from the path for now... :(
|
||
|
||
// If we're working on the same document, try not to reload.
|
||
if (self.isPreviewReady && [self.currentBaseUrl isEqualTo:baseUrl])
|
||
{
|
||
// HACK: Ideally we should only inject the parts that changed, and only
|
||
// get the parts we need. For now we only get a complete HTML codument,
|
||
// and rely on regex to get the parts we want in the DOM.
|
||
|
||
// Use the existing tree if available, and replace the content.
|
||
DOMDocument *doc = self.preview.mainFrame.DOMDocument;
|
||
DOMNodeList *htmlNodes = [doc getElementsByTagName:@"html"];
|
||
if (htmlNodes.length >= 1)
|
||
{
|
||
static NSString *pattern = @"<html>(.*)</html>";
|
||
static int opts = NSRegularExpressionDotMatchesLineSeparators;
|
||
|
||
// Find things inside the <html> tag.
|
||
NSRegularExpression *regex =
|
||
[[NSRegularExpression alloc] initWithPattern:pattern
|
||
options:opts error:NULL];
|
||
NSTextCheckingResult *result =
|
||
[regex firstMatchInString:html options:0
|
||
range:NSMakeRange(0, html.length)];
|
||
html = [html substringWithRange:[result rangeAtIndex:1]];
|
||
|
||
// Replace everything in the old <html> tag.
|
||
DOMElement *htmlNode = (DOMElement *)[htmlNodes item:0];
|
||
htmlNode.innerHTML = html;
|
||
|
||
return;
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// Reload the page if there's not valid tree to work with.
|
||
[self.preview.mainFrame loadHTMLString:html baseURL:baseUrl];
|
||
self.currentBaseUrl = baseUrl;
|
||
}
|
||
|
||
|
||
#pragma mark - Notification handler
|
||
|
||
- (void)editorTextDidChange:(NSNotification *)notification
|
||
{
|
||
if (self.needsHtml)
|
||
[self.renderer parseAndRenderLater];
|
||
}
|
||
|
||
- (void)userDefaultsDidChange:(NSNotification *)notification
|
||
{
|
||
MPRenderer *renderer = self.renderer;
|
||
|
||
// Force update if we're switching from manual to auto, or renderer settings
|
||
// changed.
|
||
int rendererFlags = self.preferences.rendererFlags;
|
||
if ((!self.preferences.markdownManualRender && self.manualRender)
|
||
|| renderer.rendererFlags != rendererFlags)
|
||
{
|
||
renderer.rendererFlags = rendererFlags;
|
||
[renderer parseAndRenderLater];
|
||
}
|
||
else
|
||
{
|
||
[renderer parseIfPreferencesChanged];
|
||
[renderer renderIfPreferencesChanged];
|
||
}
|
||
}
|
||
|
||
- (void)editorFrameDidChange:(NSNotification *)notification
|
||
{
|
||
if (self.preferences.editorWidthLimited)
|
||
[self adjustEditorInsets];
|
||
}
|
||
|
||
- (void)willStartLiveScroll:(NSNotification *)notification
|
||
{
|
||
[self updateHeaderLocations];
|
||
_inLiveScroll = YES;
|
||
}
|
||
|
||
-(void)didEndLiveScroll:(NSNotification *)notification
|
||
{
|
||
_inLiveScroll = NO;
|
||
}
|
||
|
||
- (void)editorBoundsDidChange:(NSNotification *)notification
|
||
{
|
||
if (!self.shouldHandleBoundsChange)
|
||
return;
|
||
|
||
if (self.preferences.editorSyncScrolling)
|
||
{
|
||
@synchronized(self) {
|
||
self.shouldHandleBoundsChange = NO;
|
||
if(!_inLiveScroll){
|
||
[self updateHeaderLocations];
|
||
}
|
||
|
||
[self syncScrollers];
|
||
self.shouldHandleBoundsChange = YES;
|
||
}
|
||
}
|
||
}
|
||
|
||
- (void)didRequestEditorReload:(NSNotification *)notification
|
||
{
|
||
NSString *key =
|
||
notification.userInfo[MPDidRequestEditorSetupNotificationKeyName];
|
||
[self setupEditor:key];
|
||
}
|
||
|
||
- (void)didRequestPreviewReload:(NSNotification *)notification
|
||
{
|
||
[self render:nil];
|
||
}
|
||
|
||
- (void)previewDidLiveScroll:(NSNotification *)notification
|
||
{
|
||
NSClipView *contentView = self.preview.enclosingScrollView.contentView;
|
||
self.lastPreviewScrollTop = contentView.bounds.origin.y;
|
||
}
|
||
|
||
|
||
#pragma mark - KVO
|
||
|
||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
|
||
change:(NSDictionary *)change context:(void *)context
|
||
{
|
||
if (object == self.editor)
|
||
{
|
||
if (!self.highlighter.isActive)
|
||
return;
|
||
id value = change[NSKeyValueChangeNewKey];
|
||
NSString *preferenceKey = MPEditorPreferenceKeyWithValueKey(keyPath);
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
[defaults setObject:value forKey:preferenceKey];
|
||
}
|
||
else if (object == [NSUserDefaults standardUserDefaults])
|
||
{
|
||
if (self.highlighter.isActive)
|
||
[self setupEditor:keyPath];
|
||
[self redrawDivider];
|
||
}
|
||
}
|
||
|
||
|
||
#pragma mark - IBAction
|
||
|
||
- (IBAction)copyHtml:(id)sender
|
||
{
|
||
// Dis-select things in WebView so that it's more obvious we're NOT
|
||
// respecting the selection range.
|
||
[self.preview setSelectedDOMRange:nil affinity:NSSelectionAffinityUpstream];
|
||
|
||
// If the preview is hidden, the HTML are not updating on text change.
|
||
// Perform one extra rendering so that the HTML is up to date, and do the
|
||
// copy in the rendering callback.
|
||
if (!self.needsHtml)
|
||
{
|
||
self.copying = YES;
|
||
[self.renderer parseAndRenderNow];
|
||
return;
|
||
}
|
||
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
|
||
[pasteboard clearContents];
|
||
[pasteboard writeObjects:@[self.renderer.currentHtml]];
|
||
}
|
||
|
||
- (IBAction)exportHtml:(id)sender
|
||
{
|
||
NSSavePanel *panel = [NSSavePanel savePanel];
|
||
panel.allowedFileTypes = @[@"html"];
|
||
if (self.presumedFileName)
|
||
panel.nameFieldStringValue = self.presumedFileName;
|
||
|
||
MPExportPanelAccessoryViewController *controller =
|
||
[[MPExportPanelAccessoryViewController alloc] init];
|
||
controller.stylesIncluded = (BOOL)self.preferences.htmlStyleName;
|
||
controller.highlightingIncluded = self.preferences.htmlSyntaxHighlighting;
|
||
panel.accessoryView = controller.view;
|
||
|
||
NSWindow *w = self.windowForSheet;
|
||
[panel beginSheetModalForWindow:w completionHandler:^(NSInteger result) {
|
||
if (result != NSFileHandlingPanelOKButton)
|
||
return;
|
||
BOOL styles = controller.stylesIncluded;
|
||
BOOL highlighting = controller.highlightingIncluded;
|
||
NSString *html = [self.renderer HTMLForExportWithStyles:styles
|
||
highlighting:highlighting];
|
||
[html writeToURL:panel.URL atomically:NO encoding:NSUTF8StringEncoding
|
||
error:NULL];
|
||
}];
|
||
}
|
||
|
||
- (IBAction)exportPdf:(id)sender
|
||
{
|
||
NSSavePanel *panel = [NSSavePanel savePanel];
|
||
panel.allowedFileTypes = @[@"pdf"];
|
||
if (self.presumedFileName)
|
||
panel.nameFieldStringValue = self.presumedFileName;
|
||
|
||
NSWindow *w = nil;
|
||
NSArray *windowControllers = self.windowControllers;
|
||
if (windowControllers.count > 0)
|
||
w = [windowControllers[0] window];
|
||
|
||
[panel beginSheetModalForWindow:w completionHandler:^(NSInteger result) {
|
||
if (result != NSFileHandlingPanelOKButton)
|
||
return;
|
||
|
||
NSDictionary *settings = @{
|
||
NSPrintJobDisposition: NSPrintSaveJob,
|
||
NSPrintJobSavingURL: panel.URL,
|
||
};
|
||
[self printDocumentWithSettings:settings showPrintPanel:NO delegate:nil
|
||
didPrintSelector:NULL contextInfo:NULL];
|
||
}];
|
||
}
|
||
|
||
- (IBAction)convertToH1:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:1];
|
||
}
|
||
|
||
- (IBAction)convertToH2:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:2];
|
||
}
|
||
|
||
- (IBAction)convertToH3:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:3];
|
||
}
|
||
|
||
- (IBAction)convertToH4:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:4];
|
||
}
|
||
|
||
- (IBAction)convertToH5:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:5];
|
||
}
|
||
|
||
- (IBAction)convertToH6:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:6];
|
||
}
|
||
|
||
- (IBAction)convertToParagraph:(id)sender
|
||
{
|
||
[self.editor makeHeaderForSelectedLinesWithLevel:0];
|
||
}
|
||
|
||
- (IBAction)toggleStrong:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"**" suffix:@"**"];
|
||
}
|
||
|
||
- (IBAction)toggleEmphasis:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"*" suffix:@"*"];
|
||
}
|
||
|
||
- (IBAction)toggleInlineCode:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"`" suffix:@"`"];
|
||
}
|
||
|
||
- (IBAction)toggleStrikethrough:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"~~" suffix:@"~~"];
|
||
}
|
||
|
||
- (IBAction)toggleUnderline:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"_" suffix:@"_"];
|
||
}
|
||
|
||
- (IBAction)toggleHighlight:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"==" suffix:@"=="];
|
||
}
|
||
|
||
- (IBAction)toggleComment:(id)sender
|
||
{
|
||
[self.editor toggleForMarkupPrefix:@"<!--" suffix:@"-->"];
|
||
}
|
||
|
||
- (IBAction)toggleLink:(id)sender
|
||
{
|
||
BOOL inserted = [self.editor toggleForMarkupPrefix:@"[" suffix:@"]()"];
|
||
if (!inserted)
|
||
return;
|
||
|
||
NSRange selectedRange = self.editor.selectedRange;
|
||
NSUInteger location = selectedRange.location + selectedRange.length + 2;
|
||
selectedRange = NSMakeRange(location, 0);
|
||
|
||
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
||
NSString *url = [pb URLForType:NSPasteboardTypeString].absoluteString;
|
||
if (url)
|
||
{
|
||
[self.editor insertText:url replacementRange:selectedRange];
|
||
selectedRange.length = url.length;
|
||
}
|
||
self.editor.selectedRange = selectedRange;
|
||
}
|
||
|
||
- (IBAction)toggleImage:(id)sender
|
||
{
|
||
BOOL inserted = [self.editor toggleForMarkupPrefix:@"![" suffix:@"]()"];
|
||
if (!inserted)
|
||
return;
|
||
|
||
NSRange selectedRange = self.editor.selectedRange;
|
||
NSUInteger location = selectedRange.location + selectedRange.length + 2;
|
||
selectedRange = NSMakeRange(location, 0);
|
||
|
||
NSPasteboard *pb = [NSPasteboard generalPasteboard];
|
||
NSString *url = [pb URLForType:NSPasteboardTypeString].absoluteString;
|
||
if (url)
|
||
{
|
||
[self.editor insertText:url replacementRange:selectedRange];
|
||
selectedRange.length = url.length;
|
||
}
|
||
self.editor.selectedRange = selectedRange;
|
||
}
|
||
|
||
- (IBAction)toggleOrderedList:(id)sender
|
||
{
|
||
[self.editor toggleBlockWithPattern:@"^[0-9]+ \\S" prefix:@"1. "];
|
||
}
|
||
|
||
- (IBAction)toggleUnorderedList:(id)sender
|
||
{
|
||
NSString *marker = self.preferences.editorUnorderedListMarker;
|
||
[self.editor toggleBlockWithPattern:@"^[\\*\\+-] \\S" prefix:marker];
|
||
}
|
||
|
||
- (IBAction)toggleBlockquote:(id)sender
|
||
{
|
||
[self.editor toggleBlockWithPattern:@"^> \\S" prefix:@"> "];
|
||
}
|
||
|
||
- (IBAction)indent:(id)sender
|
||
{
|
||
NSString *padding = @"\t";
|
||
if (self.preferences.editorConvertTabs)
|
||
padding = @" ";
|
||
[self.editor indentSelectedLinesWithPadding:padding];
|
||
}
|
||
|
||
- (IBAction)unindent:(id)sender
|
||
{
|
||
[self.editor unindentSelectedLines];
|
||
}
|
||
|
||
- (IBAction)insertNewParagraph:(id)sender
|
||
{
|
||
NSRange range = self.editor.selectedRange;
|
||
NSUInteger location = range.location;
|
||
NSUInteger length = range.length;
|
||
NSString *content = self.editor.string;
|
||
NSInteger newlineBefore = [content locationOfFirstNewlineBefore:location];
|
||
NSUInteger newlineAfter =
|
||
[content locationOfFirstNewlineAfter:location + length - 1];
|
||
|
||
// If we are on an empty line, treat as normal return key; otherwise insert
|
||
// two newlines.
|
||
if (location == newlineBefore + 1 && location == newlineAfter)
|
||
[self.editor insertNewline:self];
|
||
else
|
||
[self.editor insertText:@"\n\n"];
|
||
}
|
||
|
||
- (IBAction)setEditorOneQuarter:(id)sender
|
||
{
|
||
[self setSplitViewDividerLocation:0.25];
|
||
}
|
||
|
||
- (IBAction)setEditorThreeQuarters:(id)sender
|
||
{
|
||
[self setSplitViewDividerLocation:0.75];
|
||
}
|
||
|
||
- (IBAction)setEqualSplit:(id)sender
|
||
{
|
||
[self setSplitViewDividerLocation:0.5];
|
||
}
|
||
|
||
- (IBAction)toggleToolbar:(id)sender
|
||
{
|
||
[self.windowForSheet toggleToolbarShown:sender];
|
||
}
|
||
|
||
- (IBAction)togglePreviewPane:(id)sender
|
||
{
|
||
[self toggleSplitterCollapsingEditorPane:NO];
|
||
}
|
||
|
||
- (IBAction)toggleEditorPane:(id)sender
|
||
{
|
||
[self toggleSplitterCollapsingEditorPane:YES];
|
||
}
|
||
|
||
- (IBAction)render:(id)sender
|
||
{
|
||
[self.renderer parseAndRenderLater];
|
||
}
|
||
|
||
|
||
#pragma mark - Private
|
||
|
||
- (void)toggleSplitterCollapsingEditorPane:(BOOL)forEditorPane
|
||
{
|
||
BOOL isVisible = forEditorPane ? self.editorVisible : self.previewVisible;
|
||
BOOL editorOnRight = self.preferences.editorOnRight;
|
||
|
||
float targetRatio = ((forEditorPane == editorOnRight) ? 1.0 : 0.0);
|
||
|
||
if (isVisible)
|
||
{
|
||
CGFloat oldRatio = self.splitView.dividerLocation;
|
||
if (oldRatio != 0.0 && oldRatio != 1.0)
|
||
{
|
||
// We don't want to save these values, since they are meaningless.
|
||
// The user should be able to switch between 100% editor and 100%
|
||
// preview without losing the old ratio.
|
||
self.previousSplitRatio = oldRatio;
|
||
}
|
||
[self setSplitViewDividerLocation:targetRatio];
|
||
}
|
||
else
|
||
{
|
||
// We have an inconsistency here, let's just go back to 0.5,
|
||
// otherwise nothing will happen
|
||
if (self.previousSplitRatio < 0.0)
|
||
self.previousSplitRatio = 0.5;
|
||
|
||
[self setSplitViewDividerLocation:self.previousSplitRatio];
|
||
}
|
||
}
|
||
|
||
- (void)setupEditor:(NSString *)changedKey
|
||
{
|
||
[self.highlighter deactivate];
|
||
|
||
if (!changedKey || [changedKey isEqualToString:@"extensionFootnotes"])
|
||
{
|
||
int extensions = pmh_EXT_NOTES;
|
||
if (self.preferences.extensionFootnotes)
|
||
extensions = pmh_EXT_NONE;
|
||
self.highlighter.extensions = extensions;
|
||
}
|
||
|
||
if (!changedKey || [changedKey isEqualToString:@"editorHorizontalInset"]
|
||
|| [changedKey isEqualToString:@"editorVerticalInset"]
|
||
|| [changedKey isEqualToString:@"editorWidthLimited"]
|
||
|| [changedKey isEqualToString:@"editorMaximumWidth"])
|
||
{
|
||
[self adjustEditorInsets];
|
||
}
|
||
|
||
if (!changedKey || [changedKey isEqualToString:@"editorBaseFontInfo"]
|
||
|| [changedKey isEqualToString:@"editorStyleName"]
|
||
|| [changedKey isEqualToString:@"editorLineSpacing"])
|
||
{
|
||
NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
|
||
style.lineSpacing = self.preferences.editorLineSpacing;
|
||
self.editor.defaultParagraphStyle = [style copy];
|
||
NSFont *font = [self.preferences.editorBaseFont copy];
|
||
if (font)
|
||
self.editor.font = font;
|
||
self.editor.textColor = nil;
|
||
self.editor.backgroundColor = [NSColor clearColor];
|
||
self.highlighter.styles = nil;
|
||
[self.highlighter readClearTextStylesFromTextView];
|
||
|
||
NSString *themeName = [self.preferences.editorStyleName copy];
|
||
if (themeName.length)
|
||
{
|
||
NSString *path = MPThemePathForName(themeName);
|
||
NSString *themeString = MPReadFileOfPath(path);
|
||
[self.highlighter applyStylesFromStylesheet:themeString
|
||
withErrorHandler:
|
||
^(NSArray *errorMessages) {
|
||
self.preferences.editorStyleName = nil;
|
||
}];
|
||
}
|
||
|
||
CALayer *layer = [CALayer layer];
|
||
CGColorRef backgroundCGColor = self.editor.backgroundColor.CGColor;
|
||
if (backgroundCGColor)
|
||
layer.backgroundColor = backgroundCGColor;
|
||
self.editorContainer.layer = layer;
|
||
}
|
||
|
||
if ([changedKey isEqualToString:@"editorBaseFontInfo"])
|
||
{
|
||
[self scaleWebview];
|
||
}
|
||
|
||
if (!changedKey || [changedKey isEqualToString:@"editorShowWordCount"])
|
||
{
|
||
if (self.preferences.editorShowWordCount)
|
||
{
|
||
self.wordCountWidget.hidden = NO;
|
||
self.editorPaddingBottom.constant = 35.0;
|
||
[self updateWordCount];
|
||
}
|
||
else
|
||
{
|
||
self.wordCountWidget.hidden = YES;
|
||
self.editorPaddingBottom.constant = 0.0;
|
||
}
|
||
}
|
||
|
||
if (!changedKey || [changedKey isEqualToString:@"editorScrollsPastEnd"])
|
||
{
|
||
self.editor.scrollsPastEnd = self.preferences.editorScrollsPastEnd;
|
||
NSRect contentRect = self.editor.contentRect;
|
||
NSSize minSize = self.editor.enclosingScrollView.contentSize;
|
||
if (contentRect.size.height < minSize.height)
|
||
contentRect.size.height = minSize.height;
|
||
if (contentRect.size.width < minSize.width)
|
||
contentRect.size.width = minSize.width;
|
||
self.editor.frame = contentRect;
|
||
}
|
||
|
||
if (!changedKey)
|
||
{
|
||
NSClipView *contentView = self.editor.enclosingScrollView.contentView;
|
||
contentView.postsBoundsChangedNotifications = YES;
|
||
|
||
NSDictionary *keysAndDefaults = MPEditorKeysToObserve();
|
||
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
|
||
for (NSString *key in keysAndDefaults)
|
||
{
|
||
NSString *preferenceKey = MPEditorPreferenceKeyWithValueKey(key);
|
||
id value = [defaults objectForKey:preferenceKey];
|
||
value = value ? value : keysAndDefaults[key];
|
||
[self.editor setValue:value forKey:key];
|
||
}
|
||
}
|
||
|
||
if (!changedKey || [changedKey isEqualToString:@"editorOnRight"])
|
||
{
|
||
BOOL editorOnRight = self.preferences.editorOnRight;
|
||
NSArray *subviews = self.splitView.subviews;
|
||
if ((!editorOnRight && subviews[0] == self.preview)
|
||
|| (editorOnRight && subviews[1] == self.preview))
|
||
{
|
||
[self.splitView swapViews];
|
||
if (!self.previewVisible && self.previousSplitRatio >= 0.0)
|
||
self.previousSplitRatio = 1.0 - self.previousSplitRatio;
|
||
|
||
// Need to queue this or the views won't be initialised correctly.
|
||
// Don't really know why, but this works.
|
||
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
|
||
self.splitView.needsLayout = YES;
|
||
}];
|
||
}
|
||
}
|
||
|
||
[self.highlighter activate];
|
||
self.editor.automaticLinkDetectionEnabled = NO;
|
||
}
|
||
|
||
- (void)adjustEditorInsets
|
||
{
|
||
CGFloat x = self.preferences.editorHorizontalInset;
|
||
CGFloat y = self.preferences.editorVerticalInset;
|
||
if (self.preferences.editorWidthLimited)
|
||
{
|
||
CGFloat editorWidth = self.editor.frame.size.width;
|
||
CGFloat maxWidth = self.preferences.editorMaximumWidth;
|
||
if (editorWidth > 2 * x + maxWidth)
|
||
x = (editorWidth - maxWidth) * 0.45;
|
||
// We tend to expect things in an editor to shift to left a bit.
|
||
// Hence the 0.45 instead of 0.5 (which whould feel a bit too much).
|
||
}
|
||
self.editor.textContainerInset = NSMakeSize(x, y);
|
||
}
|
||
|
||
- (void)redrawDivider
|
||
{
|
||
if (!self.editorVisible)
|
||
{
|
||
// If the editor is not visible, detect preview's background color via
|
||
// DOM query and use it instead. This is more expensive; we should try
|
||
// to avoid it.
|
||
// TODO: Is it possible to cache this until the user switches the style?
|
||
// Will need to take account of the user MODIFIES the style without
|
||
// switching. Complicated. This will do for now.
|
||
self.splitView.dividerColor = MPGetWebViewBackgroundColor(self.preview);
|
||
}
|
||
else if (!self.previewVisible)
|
||
{
|
||
// If the editor is visible, match its background color.
|
||
self.splitView.dividerColor = self.editor.backgroundColor;
|
||
}
|
||
else
|
||
{
|
||
// If both sides are visible, draw a default "transparent" divider.
|
||
// This works around the possibile problem of divider's color being too
|
||
// similar to both the editor and preview and being obscured.
|
||
self.splitView.dividerColor = nil;
|
||
}
|
||
}
|
||
|
||
- (void)scaleWebview
|
||
{
|
||
if (!self.preferences.previewZoomRelativeToBaseFontSize)
|
||
return;
|
||
|
||
CGFloat fontSize = self.preferences.editorBaseFontSize;
|
||
if (fontSize <= 0.0)
|
||
return;
|
||
|
||
static const CGFloat defaultSize = 14.0;
|
||
CGFloat scale = fontSize / defaultSize;
|
||
|
||
#if 0
|
||
// Sadly, this doesn’t work correctly.
|
||
// It looks fine, but selections are offset relative to the mouse cursor.
|
||
NSScrollView *previewScrollView =
|
||
self.preview.mainFrame.frameView.documentView.enclosingScrollView;
|
||
NSClipView *previewContentView = previewScrollView.contentView;
|
||
[previewContentView scaleUnitSquareToSize:NSMakeSize(scale, scale)];
|
||
[previewContentView setNeedsDisplay:YES];
|
||
#else
|
||
// Warning: this is private webkit API and NOT App Store-safe!
|
||
[self.preview setPageSizeMultiplier:scale];
|
||
#endif
|
||
}
|
||
|
||
-(void) updateHeaderLocations
|
||
{
|
||
CGFloat offset = NSMinY(self.preview.enclosingScrollView.contentView.bounds);
|
||
NSMutableArray<NSNumber *> *locations = [NSMutableArray array];
|
||
|
||
_webViewHeaderLocations = [[self.preview.mainFrame.javaScriptContext evaluateScript:@"var arr = Array.prototype.slice.call(document.querySelectorAll(\"h1, h2, h3, h4, h5, h6, img:only-child\")); arr.map(function(n){ return n.getBoundingClientRect().top })"] toArray];
|
||
|
||
// add offset to all numbers
|
||
for (NSNumber *location in _webViewHeaderLocations)
|
||
{
|
||
[locations addObject:@([location floatValue] + offset)];
|
||
}
|
||
|
||
_webViewHeaderLocations = [locations copy];
|
||
|
||
|
||
// Next, cache the locations of all of the reference nodes in the editor view.
|
||
NSInteger characterCount = 0;
|
||
NSLayoutManager *layoutManager = [self.editor layoutManager];
|
||
NSArray<NSString *> *documentLines = [self.editor.string componentsSeparatedByString:@"\n"];
|
||
[locations removeAllObjects];
|
||
|
||
// These are the patterns for markdown headers and images respectively. we're only going to
|
||
// handle images that are not inline with other text/images
|
||
NSRegularExpression *dashRegex = [NSRegularExpression regularExpressionWithPattern:@"^([-]+)$" options:0 error:nil];
|
||
NSRegularExpression *headerRegex = [NSRegularExpression regularExpressionWithPattern:@"^(#+)\\s" options:0 error:nil];
|
||
NSRegularExpression *imgRegex = [NSRegularExpression regularExpressionWithPattern:@"^!\\[[^\\]]*\\]\\([^)]*\\)$" options:0 error:nil];
|
||
BOOL previousLineHadContent = NO;
|
||
|
||
CGFloat editorContentHeight = ceilf(NSHeight(self.editor.enclosingScrollView.documentView.bounds));
|
||
CGFloat editorVisibleHeight = ceilf(NSHeight(self.editor.enclosingScrollView.contentView.bounds));
|
||
|
||
// We start by splitting our document into lines, and then searching
|
||
// line by line for headers or images.
|
||
for (NSInteger lineNumber = 0; lineNumber < [documentLines count]; lineNumber++)
|
||
{
|
||
NSString *line = documentLines[lineNumber];
|
||
|
||
if ((previousLineHadContent && [dashRegex numberOfMatchesInString:line options:0 range:NSMakeRange(0, [line length])]) ||
|
||
[imgRegex numberOfMatchesInString:line options:0 range:NSMakeRange(0, [line length])] ||
|
||
[headerRegex numberOfMatchesInString:line options:0 range:NSMakeRange(0, [line length])])
|
||
{
|
||
// Calculate where this header/image appears vertically in the editor
|
||
NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:NSMakeRange(characterCount, [line length]) actualCharacterRange:nil];
|
||
NSRect topRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:[self.editor textContainer]];
|
||
CGFloat headerY = NSMidY(topRect);
|
||
|
||
if(headerY <= editorContentHeight - editorVisibleHeight){
|
||
[locations addObject:@(headerY)];
|
||
}
|
||
}
|
||
|
||
previousLineHadContent = [line length] && ![dashRegex numberOfMatchesInString:line options:0 range:NSMakeRange(0, [line length])];
|
||
|
||
characterCount += [line length] + 1;
|
||
}
|
||
|
||
_editorHeaderLocations = [locations copy];
|
||
}
|
||
|
||
- (void)syncScrollers
|
||
{
|
||
CGFloat editorContentHeight = ceilf(NSHeight(self.editor.enclosingScrollView.documentView.bounds));
|
||
CGFloat editorVisibleHeight = ceilf(NSHeight(self.editor.enclosingScrollView.contentView.bounds));
|
||
CGFloat previewContentHeight = ceilf(NSHeight(self.preview.enclosingScrollView.documentView.bounds));
|
||
CGFloat previewVisibleHeight = ceilf(NSHeight(self.preview.enclosingScrollView.contentView.bounds));
|
||
NSInteger relativeHeaderIndex = -1; // -1 is start of document, before any other header
|
||
CGFloat currY = NSMinY(self.editor.enclosingScrollView.contentView.bounds);
|
||
CGFloat minY = 0;
|
||
CGFloat maxY = 0;
|
||
|
||
// align the documents at the middle of the screen, except at top/bottom of document
|
||
CGFloat topTaper = MAX(0, MIN(1.0, currY / editorVisibleHeight));
|
||
CGFloat bottomTaper = 1.0 - MAX(0, MIN(1.0, (currY - editorContentHeight + 2 * editorVisibleHeight) / editorVisibleHeight));
|
||
CGFloat adjustmentForScroll = topTaper * bottomTaper * editorVisibleHeight / 2;
|
||
|
||
// We start by splitting our document into lines, and then searching
|
||
// line by line for headers or images.
|
||
for (NSNumber *headerYNum in _editorHeaderLocations) {
|
||
CGFloat headerY = [headerYNum floatValue];
|
||
headerY -= adjustmentForScroll;
|
||
|
||
if (headerY < currY)
|
||
{
|
||
// The header is before our current scroll position. the closest
|
||
// of these will be our first reference node
|
||
relativeHeaderIndex += 1;
|
||
minY = headerY;
|
||
} else if (maxY == 0 && headerY < editorContentHeight - editorVisibleHeight)
|
||
{
|
||
// Skip any headers that are within the last screen of the editor.
|
||
// we'll interpolate to the end of the document in that case.
|
||
maxY = headerY;
|
||
}
|
||
}
|
||
|
||
// Usually, we'll be scrolling between two reference nodes, but toward the end
|
||
// of the document we'll ignore nodes and reference the end of the document instead
|
||
BOOL interpolateToEndOfDocument = NO;
|
||
|
||
if (maxY == 0)
|
||
{
|
||
// We only have a reference node before our current position,
|
||
// but not after, so we'll use the end of the document.
|
||
maxY = editorContentHeight - editorVisibleHeight + adjustmentForScroll;
|
||
interpolateToEndOfDocument = YES;
|
||
}
|
||
|
||
// We are currently at currY offset, between minY and maxY, which represent
|
||
// headers indexed by relativeHeaderIndex and relativeHeaderIndex+1.
|
||
currY = MAX(0, currY - minY);
|
||
maxY -= minY;
|
||
minY -= minY;
|
||
CGFloat percentScrolledBetweenHeaders = MAX(0, MIN(1.0, currY / maxY));
|
||
|
||
// Now that we know where the editor position is relative to two reference nodes,
|
||
// we need to find the positions of those nodes in the HTML preview
|
||
CGFloat topHeaderY = 0;
|
||
CGFloat bottomHeaderY = previewContentHeight - previewVisibleHeight;
|
||
|
||
// Find the Y positions in the preview window that we're scrolling between
|
||
if ([_webViewHeaderLocations count] > relativeHeaderIndex)
|
||
{
|
||
topHeaderY = floorf([_webViewHeaderLocations[relativeHeaderIndex] doubleValue]) - adjustmentForScroll;
|
||
}
|
||
|
||
if (!interpolateToEndOfDocument && [_webViewHeaderLocations count] > relativeHeaderIndex + 1)
|
||
{
|
||
bottomHeaderY = ceilf([_webViewHeaderLocations[relativeHeaderIndex + 1] doubleValue]) - adjustmentForScroll;
|
||
}
|
||
|
||
// Now we scroll percentScrolledBetweenHeaders percent between those two positions in the webview
|
||
CGFloat previewY = topHeaderY + (bottomHeaderY - topHeaderY) * percentScrolledBetweenHeaders;
|
||
NSRect contentBounds = self.preview.enclosingScrollView.contentView.bounds;
|
||
contentBounds.origin.y = previewY;
|
||
self.preview.enclosingScrollView.contentView.bounds = contentBounds;
|
||
}
|
||
|
||
- (void)setSplitViewDividerLocation:(CGFloat)ratio
|
||
{
|
||
BOOL wasVisible = self.previewVisible;
|
||
[self.splitView setDividerLocation:ratio];
|
||
if (!wasVisible && self.previewVisible
|
||
&& !self.preferences.markdownManualRender)
|
||
[self.renderer parseAndRenderNow];
|
||
[self setupEditor:NSStringFromSelector(@selector(editorHorizontalInset))];
|
||
}
|
||
|
||
- (NSString *)presumedFileName
|
||
{
|
||
if (self.fileURL)
|
||
return self.fileURL.lastPathComponent.stringByDeletingPathExtension;
|
||
|
||
NSString *title = nil;
|
||
NSString *string = self.editor.string;
|
||
if (self.preferences.htmlDetectFrontMatter)
|
||
title = [[[string frontMatter:NULL] objectForKey:@"title"] description];
|
||
if (title)
|
||
return title;
|
||
|
||
title = string.titleString;
|
||
if (!title)
|
||
return NSLocalizedString(@"Untitled", @"default filename if no title can be determined");
|
||
|
||
static NSRegularExpression *regex = nil;
|
||
static dispatch_once_t onceToken;
|
||
dispatch_once(&onceToken, ^{
|
||
regex = [NSRegularExpression regularExpressionWithPattern:@"[/|:]"
|
||
options:0 error:NULL];
|
||
});
|
||
|
||
NSRange range = NSMakeRange(0, title.length);
|
||
title = [regex stringByReplacingMatchesInString:title options:0 range:range
|
||
withTemplate:@"-"];
|
||
return title;
|
||
}
|
||
|
||
- (void)updateWordCount
|
||
{
|
||
DOMNodeTextCount count = self.preview.mainFrame.DOMDocument.textCount;
|
||
|
||
self.totalWords = count.words;
|
||
self.totalCharacters = count.characters;
|
||
self.totalCharactersNoSpaces = count.characterWithoutSpaces;
|
||
|
||
if (self.isPreviewReady)
|
||
self.wordCountWidget.enabled = YES;
|
||
}
|
||
|
||
- (BOOL)isCurrentBaseUrl:(NSURL *)another
|
||
{
|
||
NSString *mine = self.currentBaseUrl.absoluteBaseURLString;
|
||
NSString *theirs = another.absoluteBaseURLString;
|
||
return mine == theirs || [mine isEqualToString:theirs];
|
||
}
|
||
|
||
|
||
#define OPEN_FAIL_ALERT_INFORMATIVE NSLocalizedString( \
|
||
@"Please check the path of your link is correct. Turn on \
|
||
“Automatically create link targets” If you want MacDown to \
|
||
create nonexistent link targets for you.", \
|
||
@"preview navigation error information")
|
||
|
||
#define AUTO_CREATE_FAIL_ALERT_INFORMATIVE NSLocalizedString( \
|
||
@"MacDown can’t create a file for the clicked link because \
|
||
the current file is not saved anywhere yet. Save the \
|
||
current file somewhere to enable this feature.", \
|
||
@"preview navigation error information")
|
||
|
||
|
||
- (void)openOrCreateFileForUrl:(NSURL *)url
|
||
{
|
||
// Simply open the file if it is not local, or exists already.
|
||
BOOL file = url.isFileURL;
|
||
BOOL reachable = !file || [url checkResourceIsReachableAndReturnError:NULL];
|
||
|
||
// If the file is local but doesn't exist, check if a file with
|
||
// the .md extension exists.
|
||
if (file && !reachable && [url.pathExtension isEqualToString:@""])
|
||
{
|
||
NSURL *markdownURL = [url URLByAppendingPathExtension:@"md"];
|
||
if ([markdownURL checkResourceIsReachableAndReturnError:NULL])
|
||
{
|
||
reachable = YES;
|
||
url = markdownURL;
|
||
}
|
||
}
|
||
|
||
if (reachable)
|
||
{
|
||
[[NSWorkspace sharedWorkspace] openURL:url];
|
||
return;
|
||
}
|
||
|
||
// Show an error if the user doesn't want us to create it automatically.
|
||
if (!self.preferences.createFileForLinkTarget)
|
||
{
|
||
NSAlert *alert = [[NSAlert alloc] init];
|
||
NSString *template = NSLocalizedString(
|
||
@"File not found at path:\n%@",
|
||
@"preview navigation error message");
|
||
alert.messageText = [NSString stringWithFormat:template, url.path];
|
||
alert.informativeText = OPEN_FAIL_ALERT_INFORMATIVE;
|
||
[alert runModal];
|
||
return;
|
||
}
|
||
|
||
// We can only create a file if the current file is saved. (Why?)
|
||
if (!self.fileURL)
|
||
{
|
||
NSAlert *alert = [[NSAlert alloc] init];
|
||
NSString *template = NSLocalizedString(
|
||
@"Can’t create file:\n%@", @"preview navigation error message");
|
||
alert.messageText = [NSString stringWithFormat:template,
|
||
url.lastPathComponent];
|
||
alert.informativeText = AUTO_CREATE_FAIL_ALERT_INFORMATIVE;
|
||
[alert runModal];
|
||
}
|
||
|
||
// Try to created the file.
|
||
NSDocumentController *controller =
|
||
[NSDocumentController sharedDocumentController];
|
||
|
||
NSError *error = nil;
|
||
id doc = [controller createNewEmptyDocumentForURL:url
|
||
display:YES error:&error];
|
||
if (!doc)
|
||
{
|
||
NSAlert *alert = [[NSAlert alloc] init];
|
||
NSString *template = NSLocalizedString(
|
||
@"Can’t create file:\n%@",
|
||
@"preview navigation error message");
|
||
alert.messageText =
|
||
[NSString stringWithFormat:template, url.lastPathComponent];
|
||
template = NSLocalizedString(
|
||
@"An error occurred while creating the file:\n%@",
|
||
@"preview navigation error information");
|
||
alert.informativeText =
|
||
[NSString stringWithFormat:template, error.localizedDescription];
|
||
[alert runModal];
|
||
}
|
||
}
|
||
|
||
|
||
- (void)document:(NSDocument *)doc didPrint:(BOOL)ok context:(void *)context
|
||
{
|
||
if ([doc respondsToSelector:@selector(setPrinting:)])
|
||
((MPDocument *)doc).printing = NO;
|
||
if (context)
|
||
{
|
||
NSInvocation *invocation = (__bridge NSInvocation *)context;
|
||
if ([invocation isKindOfClass:[NSInvocation class]])
|
||
{
|
||
[invocation setArgument:&doc atIndex:0];
|
||
[invocation setArgument:&ok atIndex:1];
|
||
[invocation invoke];
|
||
}
|
||
}
|
||
}
|
||
|
||
@end
|