include $(THEOS)/makefiles/common.mk | include $(THEOS)/makefiles/common.mk | ||||
TWEAK_NAME = tfdidthatsay | TWEAK_NAME = tfdidthatsay | ||||
tfdidthatsay_FILES = $(wildcard tweak/*.xm) | |||||
tweak/Narwhal.xm_CFLAGS = -fobjc-arc | |||||
tfdidthatsay_EXTRA_FRAMEWORKS += Cephei | |||||
tfdidthatsay_FILES = $(wildcard tweak/*.xm tweak/assets/*.m) | |||||
tfdidthatsay_CFLAGS = -fobjc-arc | |||||
tweak/Reddit.xm_CFLAGS = -fno-objc-arc | |||||
tweak/Apollo.xm_CFLAGS = -fno-objc-arc | |||||
include $(THEOS_MAKE_PATH)/tweak.mk | include $(THEOS_MAKE_PATH)/tweak.mk | ||||
SUBPROJECTS += prefs | SUBPROJECTS += prefs | ||||
include $(THEOS_MAKE_PATH)/aggregate.mk | include $(THEOS_MAKE_PATH)/aggregate.mk | ||||
after-install:: | after-install:: | ||||
install.exec "killall -9 Reddit" | install.exec "killall -9 Reddit" | ||||
install.exec "killall -9 Apollo" | install.exec "killall -9 Apollo" | ||||
install.exec "killall -9 narwhal" | install.exec "killall -9 narwhal" | ||||
install.exec "killall -9 AlienBlue" |
<key>label</key> | <key>label</key> | ||||
<string>Enable Narwhal</string> | <string>Enable Narwhal</string> | ||||
</dict> | </dict> | ||||
<dict> | |||||
<key>PostNotification</key> | |||||
<string>com.lint.undelete.prefs.changed</string> | |||||
<key>cell</key> | |||||
<string>PSSwitchCell</string> | |||||
<key>default</key> | |||||
<true/> | |||||
<key>defaults</key> | |||||
<string>com.lint.undelete.prefs</string> | |||||
<key>key</key> | |||||
<string>isAlienBlueEnabled</string> | |||||
<key>label</key> | |||||
<string>Enable Alien Blue</string> | |||||
</dict> | |||||
<dict> | <dict> | ||||
<key>cell</key> | <key>cell</key> | ||||
<string>PSGroupCell</string> | <string>PSGroupCell</string> | ||||
<key>footerText</key> | |||||
<string>On apps that have the eye button rather than in the menu, enable or disable that button from appearing on only deleted comments/posts.</string> | |||||
<key>label</key> | <key>label</key> | ||||
<string>Apollo </string> | |||||
<string>visibility</string> | |||||
</dict> | </dict> | ||||
<dict> | <dict> | ||||
<key>PostNotification</key> | <key>PostNotification</key> | ||||
<key>key</key> | <key>key</key> | ||||
<string>isApolloDeletedCommentsOnly</string> | <string>isApolloDeletedCommentsOnly</string> | ||||
<key>label</key> | <key>label</key> | ||||
<string>Only display eye on deleted comments</string> | |||||
<string>Apollo | Only on deleted comments</string> | |||||
</dict> | |||||
<dict> | |||||
<key>PostNotification</key> | |||||
<string>com.lint.undelete.prefs.changed</string> | |||||
<key>cell</key> | |||||
<string>PSSwitchCell</string> | |||||
<key>default</key> | |||||
<true/> | |||||
<key>defaults</key> | |||||
<string>com.lint.undelete.prefs</string> | |||||
<key>key</key> | |||||
<string>isAlienBlueDeletedOnly</string> | |||||
<key>label</key> | |||||
<string>Alien Blue | Only on deleted comments/posts</string> | |||||
</dict> | </dict> | ||||
<dict> | <dict> | ||||
<key>cell</key> | <key>cell</key> |
/* -- Voteable Interfaces -- */ | |||||
@interface VoteableElement | |||||
@property(strong, nonatomic) NSString *ident; | |||||
@property(strong, nonatomic) NSString *author; | |||||
@end | |||||
@interface Comment : VoteableElement | |||||
@property(strong, nonatomic) NSString *body; | |||||
@property(strong, nonatomic) NSString *bodyHTML; | |||||
@end | |||||
@interface Post : VoteableElement | |||||
@property(strong, nonatomic) NSString *selftext; | |||||
@property(strong, nonatomic) NSString *selftextHtml; | |||||
@property(assign, nonatomic) BOOL selfPost; | |||||
@end | |||||
/* -- UI Interfaces -- */ | |||||
@interface NCommentCell | |||||
@property(strong, nonatomic) Comment *comment; | |||||
@end | |||||
@interface NCommentPostHeaderCell | |||||
@property(strong, nonatomic) Post *post; | |||||
@property(strong, nonatomic) id node; | |||||
@end | |||||
@interface CommentsViewController | |||||
-(void) respondToStyleChange; | |||||
@end | |||||
@interface CommentPostHeaderNode | |||||
@property(strong, nonatomic) Comment *comment; | |||||
@property(strong, nonatomic) Post *post; | |||||
@end | |||||
@interface CommentOptionsDrawerView | |||||
@property(strong, nonatomic) NSMutableArray *buttons; | |||||
@property(assign, nonatomic) BOOL isPostHeader; | |||||
@property(strong, nonatomic) id delegate; | |||||
-(void) addButton:(id) arg1; | |||||
//custom elements | |||||
@property(strong, nonatomic) Comment *comment; | |||||
@property(strong, nonatomic) Post *post; | |||||
@property(strong, nonatomic) CommentPostHeaderNode *commentNode; | |||||
@end | |||||
/* -- Other Interfaces -- */ | |||||
@interface MarkupEngine | |||||
+(id) markDownHTML:(id) arg1 forSubreddit:(id) arg2; | |||||
@end | |||||
@interface Resources | |||||
+(BOOL) isNight; | |||||
@end |
#import "AlienBlue.h" | |||||
#import "assets/MMMarkdown.h" | |||||
static BOOL isAlienBlueEnabled; | |||||
static BOOL isAlienBlueDeletedOnly; | |||||
static CGFloat pushshiftRequestTimeoutValue; | |||||
%group AlienBlue | |||||
%hook NCommentCell | |||||
-(void) setDrawerView:(id) arg1 { | |||||
[arg1 setComment:[self comment]]; | |||||
%orig; | |||||
} | |||||
%end | |||||
%hook NCommentPostHeaderCell | |||||
-(void) setDrawerView:(id) arg1 { | |||||
[arg1 setPost:[self post]]; | |||||
[arg1 setCommentNode:[self node]]; | |||||
%orig; | |||||
} | |||||
%end | |||||
%hook CommentOptionsDrawerView | |||||
%property(strong, nonatomic) Comment *comment; | |||||
%property(strong, nonatomic) Post *post; | |||||
%property(strong, nonatomic) CommentPostHeaderNode *commentNode; | |||||
-(id) initWithNode:(id) arg1 { | |||||
id orig = %orig; | |||||
NSString *body; | |||||
if ([self post]) { | |||||
body = [[self post] selftext]; | |||||
} else if ([self comment]){ | |||||
body = [[self comment] body]; | |||||
} | |||||
if ((isAlienBlueDeletedOnly && ([body isEqualToString:@"[deleted]"] || [body isEqualToString:@"[removed]"])) || !isAlienBlueDeletedOnly) { | |||||
CGSize refSize = [[self buttons][0] frame].size; | |||||
UIButton *undeleteButton = [UIButton buttonWithType:UIButtonTypeCustom]; | |||||
[undeleteButton setFrame:CGRectMake(0, 0, refSize.width, refSize.height)]; | |||||
if ([self isPostHeader]){ | |||||
[undeleteButton addTarget:self action:@selector(didTapPostUndeleteButton) forControlEvents:UIControlEventTouchUpInside]; | |||||
} else { | |||||
[undeleteButton addTarget:self action:@selector(didTapCommentUndeleteButton) forControlEvents:UIControlEventTouchUpInside]; | |||||
} | |||||
if ([%c(Resources) isNight]) { | |||||
[undeleteButton setImage:[UIImage imageWithContentsOfFile:@"/var/mobile/Library/Application Support/TFDidThatSay/eye160dark.png"] forState:UIControlStateNormal]; | |||||
} else { | |||||
[undeleteButton setImage:[UIImage imageWithContentsOfFile:@"/var/mobile/Library/Application Support/TFDidThatSay/eye160light.png"] forState:UIControlStateNormal]; | |||||
} | |||||
undeleteButton.imageView.contentMode = UIViewContentModeScaleAspectFit; | |||||
undeleteButton.imageEdgeInsets = UIEdgeInsetsMake(10, 10, 10, 10); | |||||
[self addButton:undeleteButton]; | |||||
} | |||||
return orig; | |||||
} | |||||
%new | |||||
-(void) didTapCommentUndeleteButton { | |||||
Comment *comment = [self comment]; | |||||
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; | |||||
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; | |||||
[request setURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://api.pushshift.io/reddit/search/comment/?ids=%@&fields=author,body",[comment ident]]]]; | |||||
[request setHTTPMethod:@"GET"]; | |||||
[request setTimeoutInterval:pushshiftRequestTimeoutValue]; | |||||
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { | |||||
NSString *author = @"[author]"; | |||||
NSString *body = @"[body]"; | |||||
if (data != nil && error == nil){ | |||||
id jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; | |||||
if ([[jsonData objectForKey:@"data"] count] != 0){ | |||||
author = [[jsonData objectForKey:@"data"][0] objectForKey:@"author"]; | |||||
body = [[jsonData objectForKey:@"data"][0] objectForKey:@"body"]; | |||||
if ([body isEqualToString:@"[deleted]"] || [body isEqualToString:@"[removed]"]){ | |||||
body = @"[pushshift was unable to archive this]"; | |||||
} | |||||
} else { | |||||
body = @"[pushshift has not archived this yet]"; | |||||
} | |||||
} else if (error != nil || data == nil){ | |||||
body = [NSString stringWithFormat:@"[an error occured while attempting to contact pushshift api (%@)]", [error localizedDescription]]; | |||||
} | |||||
NSString *bodyHTML = [%c(MMMarkdown) HTMLStringWithMarkdown:body extensions:MMMarkdownExtensionsGitHubFlavored error:nil]; | |||||
[comment setAuthor:author]; | |||||
[comment setBody:body]; | |||||
[comment setBodyHTML:bodyHTML]; | |||||
[[self delegate] performSelectorOnMainThread:@selector(respondToStyleChange) withObject:nil waitUntilDone:NO]; | |||||
}]; | |||||
} | |||||
%new | |||||
-(void) didTapPostUndeleteButton { | |||||
Post *post = [self post]; | |||||
Comment *postComment = [[self commentNode] comment]; //Don't know why he used a comment to store info about a post, but it exists | |||||
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; | |||||
NSOperationQueue *queue = [[NSOperationQueue alloc] init]; | |||||
[request setURL:[NSURL URLWithString:[NSString stringWithFormat:@"https://api.pushshift.io/reddit/search/submission/?ids=%@&fields=author,selftext",[post ident]]]]; | |||||
[request setHTTPMethod:@"GET"]; | |||||
[request setTimeoutInterval:pushshiftRequestTimeoutValue]; | |||||
[NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { | |||||
NSString *author = @"[author]"; | |||||
NSString *body = @"[body]"; | |||||
if (data != nil && error == nil){ | |||||
id jsonData = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; | |||||
if ([[jsonData objectForKey:@"data"] count] != 0){ | |||||
author = [[jsonData objectForKey:@"data"][0] objectForKey:@"author"]; | |||||
body = [[jsonData objectForKey:@"data"][0] objectForKey:@"selftext"]; | |||||
if ([body isEqualToString:@"[deleted]"] || [body isEqualToString:@"[removed]"]){ | |||||
body = @"[pushshift was unable to archive this]"; | |||||
} | |||||
} else { | |||||
body = @"[pushshift has not archived this yet]"; | |||||
} | |||||
} else if (error != nil || data == nil){ | |||||
body = [NSString stringWithFormat:@"[an error occured while attempting to contact pushshift api (%@)]", [error localizedDescription]]; | |||||
} | |||||
NSString *bodyHTML = [%c(MMMarkdown) HTMLStringWithMarkdown:body extensions:MMMarkdownExtensionsGitHubFlavored error:nil]; | |||||
[post setAuthor:author]; | |||||
[post setSelftext:body]; | |||||
[postComment setBodyHTML:bodyHTML]; | |||||
[[self delegate] performSelectorOnMainThread:@selector(respondToStyleChange) withObject:nil waitUntilDone:NO]; | |||||
}]; | |||||
} | |||||
%end | |||||
%end | |||||
static void loadPrefs(){ | |||||
NSMutableDictionary *prefs = [[NSMutableDictionary alloc] initWithContentsOfFile:@"/User/Library/Preferences/com.lint.undelete.prefs.plist"]; | |||||
if (prefs){ | |||||
if ([prefs objectForKey:@"isAlienBlueEnabled"] != nil){ | |||||
isAlienBlueEnabled = [[prefs objectForKey:@"isAlienBlueEnabled"] boolValue]; | |||||
} else { | |||||
isAlienBlueEnabled = YES; | |||||
} | |||||
if ([prefs objectForKey:@"isAlienBlueDeletedOnly"] != nil){ | |||||
isAlienBlueDeletedOnly = [[prefs objectForKey:@"isAlienBlueDeletedOnly"] boolValue]; | |||||
} else { | |||||
isAlienBlueDeletedOnly = YES; | |||||
} | |||||
if ([prefs objectForKey:@"requestTimeoutValue"] != nil){ | |||||
pushshiftRequestTimeoutValue = [[prefs objectForKey:@"requestTimeoutValue"] doubleValue]; | |||||
} else { | |||||
pushshiftRequestTimeoutValue = 10; | |||||
} | |||||
} else { | |||||
isAlienBlueEnabled = YES; | |||||
isAlienBlueDeletedOnly = YES; | |||||
pushshiftRequestTimeoutValue = 10; | |||||
} | |||||
} | |||||
static void prefsChanged(CFNotificationCenterRef center, void *observer, CFStringRef name, const void *object, CFDictionaryRef userInfo) { | |||||
loadPrefs(); | |||||
} | |||||
%ctor { | |||||
loadPrefs(); | |||||
NSString* processName = [[NSProcessInfo processInfo] processName]; | |||||
if ([processName isEqualToString:@"AlienBlue"]){ | |||||
if (isAlienBlueEnabled){ | |||||
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, prefsChanged, CFSTR("com.lint.undelete.prefs.changed"), NULL, CFNotificationSuspensionBehaviorDeliverImmediately); | |||||
%init(AlienBlue); | |||||
} | |||||
} | |||||
} | |||||
// | |||||
// MMDocument.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
@class MMElement; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMDocument : NSObject | |||||
@property (copy, nonatomic, readonly) NSString *markdown; | |||||
@property (copy, nonatomic, readonly) NSArray<MMElement *> *elements; | |||||
+ (id)documentWithMarkdown:(NSString *)markdown; | |||||
- (id)initWithMarkdown:(NSString *)markdown; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |
// | |||||
// MMDocument.m | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMDocument.h" | |||||
#import "MMDocument_Private.h" | |||||
@interface MMDocument () | |||||
@property (copy, nonatomic) NSArray *elements; | |||||
@end | |||||
@implementation MMDocument | |||||
{ | |||||
NSMutableArray *_elements; | |||||
} | |||||
#pragma mark - Public Methods | |||||
+ (id)documentWithMarkdown:(NSString *)markdown | |||||
{ | |||||
return [[self.class alloc] initWithMarkdown:markdown]; | |||||
} | |||||
- (id)initWithMarkdown:(NSString *)markdown | |||||
{ | |||||
self = [super init]; | |||||
if (self) | |||||
{ | |||||
_markdown = markdown; | |||||
_elements = [NSMutableArray new]; | |||||
} | |||||
return self; | |||||
} | |||||
#pragma mark - Private Methods | |||||
- (void)addElement:(MMElement *)anElement | |||||
{ | |||||
[self willChangeValueForKey:@"elements"]; | |||||
[_elements addObject:anElement]; | |||||
[self didChangeValueForKey:@"elements"]; | |||||
} | |||||
@end |
// | |||||
// MMDocument_Private.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMDocument.h" | |||||
@class MMElement; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMDocument (MMDocumentPrivate) | |||||
@property (copy, nonatomic) NSArray<MMElement *> *elements; | |||||
- (void)addElement:(MMElement *)anElement; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |
// | |||||
// MMElement.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
@class MMElement; | |||||
typedef enum | |||||
{ | |||||
MMElementTypeNone, | |||||
MMElementTypeHeader, | |||||
MMElementTypeParagraph, | |||||
MMElementTypeBlockquote, | |||||
MMElementTypeNumberedList, | |||||
MMElementTypeBulletedList, | |||||
MMElementTypeListItem, | |||||
MMElementTypeCodeBlock, | |||||
MMElementTypeHorizontalRule, | |||||
MMElementTypeHTML, | |||||
MMElementTypeLineBreak, | |||||
MMElementTypeStrikethrough, | |||||
MMElementTypeStrong, | |||||
MMElementTypeEm, | |||||
MMElementTypeCodeSpan, | |||||
MMElementTypeImage, | |||||
MMElementTypeLink, | |||||
MMElementTypeMailTo, | |||||
MMElementTypeDefinition, | |||||
MMElementTypeEntity, | |||||
MMElementTypeTable, | |||||
MMElementTypeTableHeader, | |||||
MMElementTypeTableHeaderCell, | |||||
MMElementTypeTableRow, | |||||
MMElementTypeTableRowCell, | |||||
} MMElementType; | |||||
typedef NS_ENUM(NSInteger, MMTableCellAlignment) | |||||
{ | |||||
MMTableCellAlignmentNone, | |||||
MMTableCellAlignmentLeft, | |||||
MMTableCellAlignmentCenter, | |||||
MMTableCellAlignmentRight, | |||||
}; | |||||
@interface MMElement : NSObject | |||||
@property (assign, nonatomic) NSRange range; | |||||
@property (assign, nonatomic) MMElementType type; | |||||
@property (copy, nonatomic) NSArray *innerRanges; | |||||
@property (assign, nonatomic) MMTableCellAlignment alignment; | |||||
@property (assign, nonatomic) NSUInteger level; | |||||
@property (copy, nonatomic) NSString *href; | |||||
@property (copy, nonatomic) NSString *title; | |||||
@property (copy, nonatomic) NSString *identifier; | |||||
@property (copy, nonatomic) NSString *stringValue; | |||||
@property (weak, nonatomic) MMElement *parent; | |||||
@property (copy, nonatomic) NSArray<MMElement *> *children; | |||||
@property (copy, nonatomic) NSString *language; | |||||
- (void)addInnerRange:(NSRange)aRange; | |||||
- (void)removeLastInnerRange; | |||||
- (void)addChild:(MMElement *)aChild; | |||||
- (void)removeChild:(MMElement *)aChild; | |||||
- (MMElement *)removeLastChild; | |||||
@end |
// | |||||
// MMElement.m | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMElement.h" | |||||
static NSString * __MMStringFromElementType(MMElementType type) | |||||
{ | |||||
switch (type) | |||||
{ | |||||
case MMElementTypeNone: | |||||
return @"none"; | |||||
case MMElementTypeHeader: | |||||
return @"header"; | |||||
case MMElementTypeParagraph: | |||||
return @"paragraph"; | |||||
case MMElementTypeBlockquote: | |||||
return @"blockquote"; | |||||
case MMElementTypeNumberedList: | |||||
return @"ol"; | |||||
case MMElementTypeBulletedList: | |||||
return @"ul"; | |||||
case MMElementTypeListItem: | |||||
return @"li"; | |||||
case MMElementTypeCodeBlock: | |||||
return @"code"; | |||||
case MMElementTypeHorizontalRule: | |||||
return @"hr"; | |||||
case MMElementTypeHTML: | |||||
return @"html"; | |||||
case MMElementTypeLineBreak: | |||||
return @"br"; | |||||
case MMElementTypeStrikethrough: | |||||
return @"del"; | |||||
case MMElementTypeStrong: | |||||
return @"strong"; | |||||
case MMElementTypeEm: | |||||
return @"em"; | |||||
case MMElementTypeCodeSpan: | |||||
return @"code"; | |||||
case MMElementTypeImage: | |||||
return @"image"; | |||||
case MMElementTypeLink: | |||||
return @"link"; | |||||
case MMElementTypeMailTo: | |||||
return @"mailto"; | |||||
case MMElementTypeEntity: | |||||
return @"entity"; | |||||
case MMElementTypeDefinition: | |||||
return @"definition"; | |||||
default: | |||||
return @"unknown"; | |||||
} | |||||
} | |||||
@implementation MMElement | |||||
{ | |||||
NSMutableArray *_innerRanges; | |||||
NSMutableArray *_children; | |||||
} | |||||
#pragma mark - NSObject | |||||
- (id)init | |||||
{ | |||||
self = [super init]; | |||||
if (self) | |||||
{ | |||||
_innerRanges = [NSMutableArray new]; | |||||
_children = [NSMutableArray new]; | |||||
} | |||||
return self; | |||||
} | |||||
- (void)dealloc | |||||
{ | |||||
[self.children makeObjectsPerformSelector:@selector(setParent:) withObject:nil]; | |||||
} | |||||
- (NSString *)description | |||||
{ | |||||
return [NSString stringWithFormat:@"<%@: %p; type=%@; range=%@>", | |||||
NSStringFromClass(self.class), self, __MMStringFromElementType(self.type), NSStringFromRange(self.range)]; | |||||
} | |||||
#pragma mark - Public Methods | |||||
- (void)addInnerRange:(NSRange)aRange | |||||
{ | |||||
[self willChangeValueForKey:@"innerRanges"]; | |||||
[_innerRanges addObject:[NSValue valueWithRange:aRange]]; | |||||
[self didChangeValueForKey:@"innerRanges"]; | |||||
} | |||||
- (void)removeLastInnerRange | |||||
{ | |||||
[self willChangeValueForKey:@"innerRanges"]; | |||||
[_innerRanges removeLastObject]; | |||||
[self didChangeValueForKey:@"innerRanges"]; | |||||
} | |||||
- (void)addChild:(MMElement *)aChild | |||||
{ | |||||
[self willChangeValueForKey:@"children"]; | |||||
[_children addObject:aChild]; | |||||
aChild.parent = self; | |||||
[self didChangeValueForKey:@"children"]; | |||||
} | |||||
- (void)removeChild:(MMElement *)aChild | |||||
{ | |||||
[self willChangeValueForKey:@"children"]; | |||||
[_children removeObjectIdenticalTo:aChild]; | |||||
aChild.parent = nil; | |||||
[self didChangeValueForKey:@"children"]; | |||||
} | |||||
- (MMElement *)removeLastChild | |||||
{ | |||||
MMElement *child = [self.children lastObject]; | |||||
[_children removeLastObject]; | |||||
return child; | |||||
} | |||||
#pragma mark - Public Properties | |||||
- (void)setInnerRanges:(NSArray *)innerRanges | |||||
{ | |||||
_innerRanges = [innerRanges mutableCopy]; | |||||
} | |||||
- (void)setChildren:(NSArray *)children | |||||
{ | |||||
for (MMElement *child in _children) { | |||||
child.parent = nil; | |||||
} | |||||
_children = [children mutableCopy]; | |||||
for (MMElement *child in _children) { | |||||
child.parent = self; | |||||
} | |||||
} | |||||
@end |
// | |||||
// MMGenerator.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
@class MMDocument; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMGenerator : NSObject | |||||
- (NSString *)generateHTML:(MMDocument *)aDocument; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |
// | |||||
// MMGenerator.m | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMGenerator.h" | |||||
#import "MMDocument.h" | |||||
#import "MMElement.h" | |||||
// This value is used to estimate the length of the HTML output. The length of the markdown document | |||||
// is multplied by it to create an NSMutableString with an initial capacity. | |||||
static const Float64 kHTMLDocumentLengthMultiplier = 1.25; | |||||
static NSString * __HTMLEscapedString(NSString *aString) | |||||
{ | |||||
NSMutableString *result = [aString mutableCopy]; | |||||
[result replaceOccurrencesOfString:@"&" | |||||
withString:@"&" | |||||
options:NSLiteralSearch | |||||
range:NSMakeRange(0, result.length)]; | |||||
[result replaceOccurrencesOfString:@"\"" | |||||
withString:@""" | |||||
options:NSLiteralSearch | |||||
range:NSMakeRange(0, result.length)]; | |||||
return result; | |||||
} | |||||
static NSString *__obfuscatedEmailAddress(NSString *anAddress) | |||||
{ | |||||
NSMutableString *result = [NSMutableString new]; | |||||
NSString *(^decimal)(unichar c) = ^(unichar c){ return [NSString stringWithFormat:@"&#%d;", c]; }; | |||||
NSString *(^hex)(unichar c) = ^(unichar c){ return [NSString stringWithFormat:@"&#x%x;", c]; }; | |||||
NSString *(^raw)(unichar c) = ^(unichar c){ return [NSString stringWithCharacters:&c length:1]; }; | |||||
NSArray *encoders = @[ decimal, hex, raw ]; | |||||
for (NSUInteger idx=0; idx<anAddress.length; idx++) | |||||
{ | |||||
unichar character = [anAddress characterAtIndex:idx]; | |||||
NSString *(^encoder)(unichar c); | |||||
if (character == '@') | |||||
{ | |||||
// Make sure that the @ gets encoded | |||||
encoder = [encoders objectAtIndex:arc4random_uniform(2)]; | |||||
} | |||||
else | |||||
{ | |||||
int r = arc4random_uniform(100); | |||||
encoder = encoders[(r >= 90) ? 2 : (r >= 45) ? 1 : 0]; | |||||
} | |||||
[result appendString:encoder(character)]; | |||||
} | |||||
return result; | |||||
} | |||||
static NSString * __HTMLStartTagForElement(MMElement *anElement) | |||||
{ | |||||
switch (anElement.type) | |||||
{ | |||||
case MMElementTypeHeader: | |||||
return [NSString stringWithFormat:@"<h%u>", (unsigned int)anElement.level]; | |||||
case MMElementTypeParagraph: | |||||
return @"<p>"; | |||||
case MMElementTypeBulletedList: | |||||
return @"<ul>\n"; | |||||
case MMElementTypeNumberedList: | |||||
return @"<ol>\n"; | |||||
case MMElementTypeListItem: | |||||
return @"<li>"; | |||||
case MMElementTypeBlockquote: | |||||
return @"<blockquote>\n"; | |||||
case MMElementTypeCodeBlock: | |||||
return anElement.language ? [NSString stringWithFormat:@"<pre><code class=\"%@\">", anElement.language] : @"<pre><code>"; | |||||
case MMElementTypeLineBreak: | |||||
return @"<br />"; | |||||
case MMElementTypeHorizontalRule: | |||||
return @"\n<hr />\n"; | |||||
case MMElementTypeStrikethrough: | |||||
return @"<del>"; | |||||
case MMElementTypeStrong: | |||||
return @"<strong>"; | |||||
case MMElementTypeEm: | |||||
return @"<em>"; | |||||
case MMElementTypeCodeSpan: | |||||
return @"<code>"; | |||||
case MMElementTypeImage: | |||||
if (anElement.title != nil) | |||||
{ | |||||
return [NSString stringWithFormat:@"<img src=\"%@\" alt=\"%@\" title=\"%@\" />", | |||||
__HTMLEscapedString(anElement.href), | |||||
__HTMLEscapedString(anElement.stringValue), | |||||
__HTMLEscapedString(anElement.title)]; | |||||
} | |||||
return [NSString stringWithFormat:@"<img src=\"%@\" alt=\"%@\" />", | |||||
__HTMLEscapedString(anElement.href), | |||||
__HTMLEscapedString(anElement.stringValue)]; | |||||
case MMElementTypeLink: | |||||
if (anElement.title != nil) | |||||
{ | |||||
return [NSString stringWithFormat:@"<a title=\"%@\" href=\"%@\">", | |||||
__HTMLEscapedString(anElement.title), __HTMLEscapedString(anElement.href)]; | |||||
} | |||||
return [NSString stringWithFormat:@"<a href=\"%@\">", __HTMLEscapedString(anElement.href)]; | |||||
case MMElementTypeMailTo: | |||||
return [NSString stringWithFormat:@"<a href=\"%@\">%@</a>", | |||||
__obfuscatedEmailAddress([NSString stringWithFormat:@"mailto:%@", anElement.href]), | |||||
__obfuscatedEmailAddress(anElement.href)]; | |||||
case MMElementTypeEntity: | |||||
return anElement.stringValue; | |||||
case MMElementTypeTable: | |||||
return @"<table>"; | |||||
case MMElementTypeTableHeader: | |||||
return @"<thead><tr>"; | |||||
case MMElementTypeTableHeaderCell: | |||||
return anElement.alignment == MMTableCellAlignmentCenter ? @"<th align='center'>" | |||||
: anElement.alignment == MMTableCellAlignmentLeft ? @"<th align='left'>" | |||||
: anElement.alignment == MMTableCellAlignmentRight ? @"<th align='right'>" | |||||
: @"<th>"; | |||||
case MMElementTypeTableRow: | |||||
return @"<tr>"; | |||||
case MMElementTypeTableRowCell: | |||||
return anElement.alignment == MMTableCellAlignmentCenter ? @"<td align='center'>" | |||||
: anElement.alignment == MMTableCellAlignmentLeft ? @"<td align='left'>" | |||||
: anElement.alignment == MMTableCellAlignmentRight ? @"<td align='right'>" | |||||
: @"<td>"; | |||||
default: | |||||
return nil; | |||||
} | |||||
} | |||||
static NSString * __HTMLEndTagForElement(MMElement *anElement) | |||||
{ | |||||
switch (anElement.type) | |||||
{ | |||||
case MMElementTypeHeader: | |||||
return [NSString stringWithFormat:@"</h%u>\n", (unsigned int)anElement.level]; | |||||
case MMElementTypeParagraph: | |||||
return @"</p>\n"; | |||||
case MMElementTypeBulletedList: | |||||
return @"</ul>\n"; | |||||
case MMElementTypeNumberedList: | |||||
return @"</ol>\n"; | |||||
case MMElementTypeListItem: | |||||
return @"</li>\n"; | |||||
case MMElementTypeBlockquote: | |||||
return @"</blockquote>\n"; | |||||
case MMElementTypeCodeBlock: | |||||
return @"</code></pre>\n"; | |||||
case MMElementTypeStrikethrough: | |||||
return @"</del>"; | |||||
case MMElementTypeStrong: | |||||
return @"</strong>"; | |||||
case MMElementTypeEm: | |||||
return @"</em>"; | |||||
case MMElementTypeCodeSpan: | |||||
return @"</code>"; | |||||
case MMElementTypeLink: | |||||
return @"</a>"; | |||||
case MMElementTypeTable: | |||||
return @"</tbody></table>"; | |||||
case MMElementTypeTableHeader: | |||||
return @"</tr></thead><tbody>"; | |||||
case MMElementTypeTableHeaderCell: | |||||
return @"</th>"; | |||||
case MMElementTypeTableRow: | |||||
return @"</tr>"; | |||||
case MMElementTypeTableRowCell: | |||||
return @"</td>"; | |||||
default: | |||||
return nil; | |||||
} | |||||
} | |||||
@interface MMGenerator () | |||||
- (void) _generateHTMLForElement:(MMElement *)anElement | |||||
inDocument:(MMDocument *)aDocument | |||||
HTML:(NSMutableString *)theHTML | |||||
location:(NSUInteger *)aLocation; | |||||
@end | |||||
@implementation MMGenerator | |||||
#pragma mark - Public Methods | |||||
- (NSString *)generateHTML:(MMDocument *)aDocument | |||||
{ | |||||
NSString *markdown = aDocument.markdown; | |||||
NSUInteger location = 0; | |||||
NSUInteger length = markdown.length; | |||||
NSMutableString *HTML = [NSMutableString stringWithCapacity:length * kHTMLDocumentLengthMultiplier]; | |||||
for (MMElement *element in aDocument.elements) | |||||
{ | |||||
if (element.type == MMElementTypeHTML) | |||||
{ | |||||
[HTML appendString:[aDocument.markdown substringWithRange:element.range]]; | |||||
} | |||||
else | |||||
{ | |||||
[self _generateHTMLForElement:element | |||||
inDocument:aDocument | |||||
HTML:HTML | |||||
location:&location]; | |||||
} | |||||
} | |||||
return HTML; | |||||
} | |||||
#pragma mark - Private Methods | |||||
- (void)_generateHTMLForElement:(MMElement *)anElement | |||||
inDocument:(MMDocument *)aDocument | |||||
HTML:(NSMutableString *)theHTML | |||||
location:(NSUInteger *)aLocation | |||||
{ | |||||
NSString *startTag = __HTMLStartTagForElement(anElement); | |||||
NSString *endTag = __HTMLEndTagForElement(anElement); | |||||
if (startTag) | |||||
[theHTML appendString:startTag]; | |||||
for (MMElement *child in anElement.children) | |||||
{ | |||||
if (child.type == MMElementTypeNone) | |||||
{ | |||||
NSString *markdown = aDocument.markdown; | |||||
if (child.range.length == 0) | |||||
{ | |||||
[theHTML appendString:@"\n"]; | |||||
} | |||||
else | |||||
{ | |||||
[theHTML appendString:[markdown substringWithRange:child.range]]; | |||||
} | |||||
} | |||||
else if (child.type == MMElementTypeHTML) | |||||
{ | |||||
[theHTML appendString:[aDocument.markdown substringWithRange:child.range]]; | |||||
} | |||||
else | |||||
{ | |||||
[self _generateHTMLForElement:child | |||||
inDocument:aDocument | |||||
HTML:theHTML | |||||
location:aLocation]; | |||||
} | |||||
} | |||||
if (endTag) | |||||
[theHTML appendString:endTag]; | |||||
} | |||||
@end |
// | |||||
// MMHTMLParser.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
@class MMElement; | |||||
@class MMScanner; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMHTMLParser : NSObject | |||||
- (nullable MMElement *)parseBlockTagWithScanner:(MMScanner *)scanner; | |||||
- (nullable MMElement *)parseCommentWithScanner:(MMScanner *)scanner; | |||||
- (nullable MMElement *)parseInlineTagWithScanner:(MMScanner *)scanner; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |
// | |||||
// MMHTMLParser.m | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMHTMLParser.h" | |||||
#import "MMElement.h" | |||||
#import "MMScanner.h" | |||||
@implementation MMHTMLParser | |||||
#pragma mark - Public Methods | |||||
- (MMElement *)parseBlockTagWithScanner:(MMScanner *)scanner | |||||
{ | |||||
[scanner beginTransaction]; | |||||
MMElement *element = [self _parseStrictBlockTagWithScanner:scanner]; | |||||
[scanner commitTransaction:element != nil]; | |||||
if (element) | |||||
return element; | |||||
return [self _parseLenientBlockTagWithScanner:scanner]; | |||||
} | |||||
- (MMElement *)parseCommentWithScanner:(MMScanner *)scanner | |||||
{ | |||||
if (![scanner matchString:@"<!--"]) | |||||
return nil; | |||||
NSCharacterSet *setToSkip = [[NSCharacterSet characterSetWithCharactersInString:@"-"] invertedSet]; | |||||
while (!scanner.atEndOfString) | |||||
{ | |||||
if (scanner.atEndOfLine) | |||||
[scanner advanceToNextLine]; | |||||
else | |||||
{ | |||||
[scanner skipCharactersFromSet:setToSkip]; | |||||
if ([scanner matchString:@"-->"]) | |||||
{ | |||||
MMElement *element = [MMElement new]; | |||||
element.type = MMElementTypeHTML; | |||||
element.range = NSMakeRange(scanner.startLocation, scanner.location-scanner.startLocation); | |||||
return element; | |||||
} | |||||
[scanner advance]; | |||||
} | |||||
} | |||||
return nil; | |||||
} | |||||
- (MMElement *)parseInlineTagWithScanner:(MMScanner *)scanner | |||||
{ | |||||
if (scanner.nextCharacter != '<') | |||||
return nil; | |||||
[scanner advance]; | |||||
if (scanner.nextCharacter == '/') | |||||
[scanner advance]; | |||||
NSRange tagNameRange = [self _parseNameWithScanner:scanner]; | |||||
if (tagNameRange.length == 0) | |||||
return nil; | |||||
[self _parseAttributesWithScanner:scanner]; | |||||
[scanner skipWhitespace]; | |||||
if (scanner.nextCharacter == '/') | |||||
[scanner advance]; | |||||
if (scanner.nextCharacter != '>') | |||||
return nil; | |||||
[scanner advance]; | |||||
MMElement *element = [MMElement new]; | |||||
element.type = MMElementTypeHTML; | |||||
element.range = NSMakeRange(scanner.startLocation, scanner.location-scanner.startLocation); | |||||
element.stringValue = [scanner.string substringWithRange:tagNameRange]; | |||||
return element; | |||||
} | |||||
#pragma mark - Private Methods | |||||
- (MMElement *)_parseStrictBlockTagWithScanner:(MMScanner *)scanner | |||||
{ | |||||
// which starts with a '<' | |||||
if (scanner.nextCharacter != '<') | |||||
return nil; | |||||
[scanner advance]; | |||||
NSSet *htmlBlockTags = [NSSet setWithObjects: | |||||
@"p", @"div", @"h1", @"h2", @"h3", @"h4", @"h5", @"h6", | |||||
@"blockquote", @"pre", @"table", @"dl", @"ol", @"ul", | |||||
@"script", @"noscript", @"form", @"fieldset", @"iframe", | |||||
@"math", @"ins", @"del", nil]; | |||||
NSString *tagName = [scanner nextWord]; | |||||
if (![htmlBlockTags containsObject:tagName]) | |||||
return nil; | |||||
scanner.location += tagName.length; | |||||
[self _parseAttributesWithScanner:scanner]; | |||||
[scanner skipWhitespace]; | |||||
if (scanner.nextCharacter != '>') | |||||
return nil; | |||||
[scanner advance]; | |||||
NSCharacterSet *boringChars = [[NSCharacterSet characterSetWithCharactersInString:@"<"] invertedSet]; | |||||
while (1) | |||||
{ | |||||
if (scanner.atEndOfString) | |||||
return nil; | |||||
[scanner skipCharactersFromSet:boringChars]; | |||||
if (scanner.atEndOfLine) | |||||
{ | |||||
[scanner advanceToNextLine]; | |||||
continue; | |||||
} | |||||
[scanner beginTransaction]; | |||||
if ([self _parseEndTag:tagName withScanner:scanner]) | |||||
{ | |||||
[scanner commitTransaction:YES]; | |||||
break; | |||||
} | |||||
[scanner commitTransaction:NO]; | |||||
MMElement *element; | |||||
[scanner beginTransaction]; | |||||
element = [self _parseStrictBlockTagWithScanner:scanner]; | |||||
[scanner commitTransaction:element != nil]; | |||||
if (element) | |||||
continue; | |||||
[scanner beginTransaction]; | |||||
element = [self parseCommentWithScanner:scanner]; | |||||
[scanner commitTransaction:element != nil]; | |||||
if (element) | |||||
continue; | |||||
[scanner beginTransaction]; | |||||
element = [self parseInlineTagWithScanner:scanner]; | |||||
[scanner commitTransaction:element != nil]; | |||||
if (element) | |||||
continue; | |||||
return nil; | |||||
} | |||||
MMElement *element = [MMElement new]; | |||||
element.type = MMElementTypeHTML; | |||||
element.range = NSMakeRange(scanner.startLocation, scanner.location-scanner.startLocation); | |||||
return element; | |||||
} | |||||
- (BOOL)_parseEndTag:(NSString *)tagName withScanner:(MMScanner *)scanner | |||||
{ | |||||
if (scanner.nextCharacter != '<') | |||||
return NO; | |||||
[scanner advance]; | |||||
if (scanner.nextCharacter != '/') | |||||
return NO; | |||||
[scanner advance]; | |||||
[scanner skipWhitespace]; | |||||
if (![scanner matchString:tagName]) | |||||
return NO; | |||||
[scanner skipWhitespace]; | |||||
if (scanner.nextCharacter != '>') | |||||
return NO; | |||||
[scanner advance]; | |||||
return YES; | |||||
} | |||||
- (MMElement *)_parseLenientBlockTagWithScanner:(MMScanner *)scanner | |||||
{ | |||||
// which starts with a '<' | |||||
if (scanner.nextCharacter != '<') | |||||
return nil; | |||||
[scanner advance]; | |||||
NSSet *htmlBlockTags = [NSSet setWithObjects: | |||||
@"p", @"div", @"h1", @"h2", @"h3", @"h4", @"h5", @"h6", | |||||
@"blockquote", @"pre", @"table", @"dl", @"ol", @"ul", | |||||
@"script", @"noscript", @"form", @"fieldset", @"iframe", | |||||
@"math", @"ins", @"del", nil]; | |||||
NSString *tagName = scanner.nextWord; | |||||
if (![htmlBlockTags containsObject:tagName]) | |||||
return nil; | |||||
scanner.location += tagName.length; | |||||
// Find a '>' | |||||
while (scanner.nextCharacter != '>') | |||||
{ | |||||
if (scanner.atEndOfString) | |||||
return nil; | |||||
else if (scanner.atEndOfLine) | |||||
[scanner advanceToNextLine]; | |||||
else | |||||
[scanner advance]; | |||||
} | |||||
// Skip lines until we come across a blank line | |||||
while (!scanner.atEndOfLine) | |||||
{ | |||||
[scanner advanceToNextLine]; | |||||
} | |||||
MMElement *element = [MMElement new]; | |||||
element.type = MMElementTypeHTML; | |||||
element.range = NSMakeRange(scanner.startLocation, scanner.location-scanner.startLocation); | |||||
return element; | |||||
} | |||||
- (NSRange)_parseNameWithScanner:(MMScanner *)scanner | |||||
{ | |||||
NSMutableCharacterSet *nameSet = [NSMutableCharacterSet alphanumericCharacterSet]; | |||||
[nameSet addCharactersInString:@":-"]; | |||||
NSRange result = NSMakeRange(scanner.location, 0); | |||||
result.length = [scanner skipCharactersFromSet:nameSet]; | |||||
return result; | |||||
} | |||||
- (BOOL)_parseStringWithScanner:(MMScanner *)scanner | |||||
{ | |||||
unichar nextChar = [scanner nextCharacter]; | |||||
if (nextChar != '"' && nextChar != '\'') | |||||
return NO; | |||||
[scanner advance]; | |||||
while (scanner.nextCharacter != nextChar) | |||||
{ | |||||
if (scanner.atEndOfString) | |||||
return NO; | |||||
else if (scanner.atEndOfLine) | |||||
[scanner advanceToNextLine]; | |||||
else | |||||
[scanner advance]; | |||||
} | |||||
// skip over the closing quotation mark | |||||
[scanner advance]; | |||||
return YES; | |||||
} | |||||
- (BOOL)_parseAttributeValueWithScanner:(MMScanner *)scanner | |||||
{ | |||||
NSMutableCharacterSet *characters = [[NSCharacterSet.whitespaceCharacterSet invertedSet] mutableCopy]; | |||||
[characters removeCharactersInString:@"\"'=><`"]; | |||||
return [scanner skipCharactersFromSet:characters] > 0; | |||||
} | |||||
- (void)_parseAttributesWithScanner:(MMScanner *)scanner | |||||
{ | |||||
while ([scanner skipWhitespaceAndNewlines] > 0) | |||||
{ | |||||
NSRange range; | |||||
range = [self _parseNameWithScanner:scanner]; | |||||
if (range.length == 0) | |||||
break; | |||||
[scanner beginTransaction]; | |||||
[scanner skipWhitespace]; | |||||
if (scanner.nextCharacter == '=') | |||||
{ | |||||
[scanner commitTransaction:YES]; | |||||
[scanner advance]; | |||||
[scanner skipWhitespace]; | |||||
if ([self _parseStringWithScanner:scanner]) | |||||
; | |||||
else if ([self _parseAttributeValueWithScanner:scanner]) | |||||
; | |||||
else | |||||
break; | |||||
} | |||||
else | |||||
{ | |||||
[scanner commitTransaction:NO]; | |||||
} | |||||
} | |||||
} | |||||
@end |
// | |||||
// Prefix header for all source files of the 'MMMarkdown' target in the 'MMMarkdown' project | |||||
// | |||||
#ifdef __OBJC__ | |||||
#import <Foundation/Foundation.h> | |||||
#endif |
// | |||||
// MMMarkdown.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
//! Project version number for MMMarkdown. | |||||
FOUNDATION_EXPORT double MMMarkdownVersionNumber; | |||||
//! Project version string for MMMarkdown. | |||||
FOUNDATION_EXPORT const unsigned char MMMarkdownVersionString[]; | |||||
typedef NS_OPTIONS(NSUInteger, MMMarkdownExtensions) | |||||
{ | |||||
MMMarkdownExtensionsNone = 0, | |||||
MMMarkdownExtensionsAutolinkedURLs = 1 << 0, | |||||
// MMMarkdownExtensionsCrossReferences = 1 << 1, | |||||
// MMMarkdownExtensionsCustomAttributes = 1 << 2, | |||||
MMMarkdownExtensionsFencedCodeBlocks = 1 << 3, | |||||
// MMMarkdownExtensionsFootnotes = 1 << 4, | |||||
MMMarkdownExtensionsHardNewlines = 1 << 5, | |||||
MMMarkdownExtensionsStrikethroughs = 1 << 6, | |||||
// MMMarkdownExtensionsTableCaptions = 1 << 7, | |||||
MMMarkdownExtensionsTables = 1 << 8, | |||||
MMMarkdownExtensionsUnderscoresInWords = 1 << 9, | |||||
MMMarkdownExtensionsGitHubFlavored = MMMarkdownExtensionsAutolinkedURLs|MMMarkdownExtensionsFencedCodeBlocks|MMMarkdownExtensionsHardNewlines|MMMarkdownExtensionsStrikethroughs|MMMarkdownExtensionsTables|MMMarkdownExtensionsUnderscoresInWords, | |||||
}; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMMarkdown : NSObject | |||||
/*! | |||||
Convert a Markdown string to HTML. | |||||
@param string | |||||
A Markdown string. Must not be nil. | |||||
@param error | |||||
Out parameter used if an error occurs while parsing the Markdown. May be NULL. | |||||
@result | |||||
Returns an HTML string. | |||||
*/ | |||||
+ (NSString *)HTMLStringWithMarkdown:(NSString *)string error:(NSError * __autoreleasing * _Nullable)error; | |||||
/*! | |||||
Convert a Markdown string to HTML. | |||||
@param string | |||||
A Markdown string. Must not be nil. | |||||
@param extensions | |||||
The extensions to enable. | |||||
@param error | |||||
Out parameter used if an error occurs while parsing the Markdown. May be NULL. | |||||
@result | |||||
Returns an HTML string. | |||||
*/ | |||||
+ (NSString *)HTMLStringWithMarkdown:(NSString *)string extensions:(MMMarkdownExtensions)extensions error:(NSError * __autoreleasing * _Nullable)error; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |
// | |||||
// MMMarkdown.m | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMMarkdown.h" | |||||
#import "MMParser.h" | |||||
#import "MMGenerator.h" | |||||
@implementation MMMarkdown | |||||
#pragma mark - Public Methods | |||||
+ (NSString *)HTMLStringWithMarkdown:(NSString *)string error:(__autoreleasing NSError **)error | |||||
{ | |||||
return [self HTMLStringWithMarkdown:string extensions:MMMarkdownExtensionsNone fromSelector:_cmd error:error]; | |||||
} | |||||
+ (NSString *)HTMLStringWithMarkdown:(NSString *)string extensions:(MMMarkdownExtensions)extensions error:(NSError *__autoreleasing *)error | |||||
{ | |||||
return [self HTMLStringWithMarkdown:string extensions:extensions fromSelector:_cmd error:error]; | |||||
} | |||||
#pragma mark - Private Methods | |||||
+ (NSString *)HTMLStringWithMarkdown:(NSString *)string | |||||
extensions:(MMMarkdownExtensions)extensions | |||||
fromSelector:(SEL)selector | |||||
error:(__autoreleasing NSError **)error | |||||
{ | |||||
if (string == nil) | |||||
{ | |||||
NSString *reason = [NSString stringWithFormat:@"[%@ %@]: nil argument for markdown", | |||||
NSStringFromClass(self.class), NSStringFromSelector(selector)]; | |||||
@throw [NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil]; | |||||
} | |||||
if (string.length == 0) | |||||
return @""; | |||||
MMParser *parser = [[MMParser alloc] initWithExtensions:extensions]; | |||||
MMGenerator *generator = [MMGenerator new]; | |||||
MMDocument *document = [parser parseMarkdown:string error:error]; | |||||
return [generator generateHTML:document]; | |||||
} | |||||
@end |
// | |||||
// MMParser.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
#import "MMMarkdown.h" | |||||
@class MMDocument; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMParser : NSObject | |||||
- (id)initWithExtensions:(MMMarkdownExtensions)extensions; | |||||
- (MMDocument *)parseMarkdown:(NSString *)markdown error:(NSError * __autoreleasing * _Nullable)error; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |
// | |||||
// MMScanner.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
@interface MMScanner : NSObject | |||||
// Constant info | |||||
@property (strong, nonatomic, readonly) NSString *string; | |||||
@property (copy, nonatomic, readonly) NSArray *lineRanges; | |||||
// Changing | |||||
@property (assign, nonatomic, readonly) NSUInteger startLocation; | |||||
@property (assign, nonatomic, readonly) NSRange currentRange; | |||||
// Settable | |||||
@property (assign, nonatomic) NSUInteger location; | |||||
+ (id)scannerWithString:(NSString *)aString; | |||||
- (id)initWithString:(NSString *)aString; | |||||
+ (id)scannerWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges; | |||||
- (id)initWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges; | |||||
- (void)beginTransaction; | |||||
- (void)commitTransaction:(BOOL)shouldSave; | |||||
- (BOOL)atBeginningOfLine; | |||||
- (BOOL)atEndOfLine; | |||||
- (BOOL)atEndOfString; | |||||
- (unichar)previousCharacter; | |||||
- (unichar)nextCharacter; | |||||
- (NSString *)previousWord; | |||||
- (NSString *)nextWord; | |||||
- (NSString *)previousWordWithCharactersFromSet:(NSCharacterSet *)set; | |||||
- (NSString *)nextWordWithCharactersFromSet:(NSCharacterSet *)set; | |||||
- (void)advance; | |||||
- (void)advanceToNextLine; | |||||
- (BOOL)matchString:(NSString *)string; | |||||
- (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet; | |||||
- (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet max:(NSUInteger)maxToSkip; | |||||
- (NSUInteger)skipEmptyLines; | |||||
- (NSUInteger)skipIndentationUpTo:(NSUInteger)maxSpacesToSkip; | |||||
- (NSUInteger)skipNestedBracketsWithDelimiter:(unichar)delimiter; | |||||
- (NSUInteger)skipToEndOfLine; | |||||
- (NSUInteger)skipToLastCharacterOfLine; | |||||
- (NSUInteger)skipWhitespace; | |||||
- (NSUInteger)skipWhitespaceAndNewlines; | |||||
@end |
// | |||||
// MMScanner.m | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import "MMScanner.h" | |||||
static NSString *__delimitersForCharacter(unichar character) | |||||
{ | |||||
switch (character) | |||||
{ | |||||
case '[': | |||||
case ']': | |||||
return @"[]"; | |||||
case '(': | |||||
case ')': | |||||
return @"()"; | |||||
case '<': | |||||
case '>': | |||||
return @"<>"; | |||||
case '{': | |||||
case '}': | |||||
return @"{}"; | |||||
default: | |||||
[NSException raise:@"Invalid delimiter character" | |||||
format:@"Character '%C' is not a valid delimiter", character]; | |||||
return nil; | |||||
} | |||||
} | |||||
@interface MMScanner () | |||||
@property (assign, nonatomic) NSUInteger startLocation; | |||||
@property (assign, nonatomic) NSRange currentRange; | |||||
@property (assign, nonatomic, readonly) NSRange currentLineRange; | |||||
@property (assign, nonatomic) NSUInteger rangeIndex; | |||||
@property (strong, nonatomic, readonly) NSMutableArray *transactions; | |||||
@end | |||||
@implementation MMScanner | |||||
#pragma mark - Public Methods | |||||
+ (id)scannerWithString:(NSString *)aString | |||||
{ | |||||
return [[self.class alloc] initWithString:aString]; | |||||
} | |||||
- (id)initWithString:(NSString *)aString | |||||
{ | |||||
NSArray *lineRanges = [self _lineRangesForString:aString]; | |||||
return [self initWithString:aString lineRanges:lineRanges]; | |||||
} | |||||
+ (id)scannerWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges | |||||
{ | |||||
return [[self.class alloc] initWithString:aString lineRanges:theLineRanges]; | |||||
} | |||||
- (id)initWithString:(NSString *)aString lineRanges:(NSArray *)theLineRanges | |||||
{ | |||||
NSParameterAssert(theLineRanges.count > 0); | |||||
self = [super init]; | |||||
if (self) | |||||
{ | |||||
_string = aString; | |||||
_lineRanges = theLineRanges; | |||||
_rangeIndex = 0; | |||||
_transactions = [NSMutableArray new]; | |||||
self.startLocation = 0; | |||||
self.currentRange = self.currentLineRange; | |||||
} | |||||
return self; | |||||
} | |||||
- (void)beginTransaction | |||||
{ | |||||
NSDictionary *transaction = @{ | |||||
@"rangeIndex": @(self.rangeIndex), | |||||
@"location": @(self.location), | |||||
@"startLocation": @(self.startLocation), | |||||
}; | |||||
[self.transactions addObject:transaction]; | |||||
self.startLocation = self.location; | |||||
} | |||||
- (void)commitTransaction:(BOOL)shouldSave | |||||
{ | |||||
if (!self.transactions.count) | |||||
[NSException raise:@"Transaction underflow" format:@"Could not commit transaction because the stack is empty"]; | |||||
NSDictionary *transaction = [self.transactions lastObject]; | |||||
[self.transactions removeLastObject]; | |||||
self.startLocation = [[transaction objectForKey:@"startLocation"] unsignedIntegerValue]; | |||||
if (!shouldSave) | |||||
{ | |||||
self.rangeIndex = [[transaction objectForKey:@"rangeIndex"] unsignedIntegerValue]; | |||||
self.location = [[transaction objectForKey:@"location"] unsignedIntegerValue]; | |||||
} | |||||
} | |||||
- (BOOL)atBeginningOfLine | |||||
{ | |||||
return self.location == self.currentLineRange.location; | |||||
} | |||||
- (BOOL)atEndOfLine | |||||
{ | |||||
return self.location == NSMaxRange(self.currentLineRange); | |||||
} | |||||
- (BOOL)atEndOfString | |||||
{ | |||||
return self.atEndOfLine && self.rangeIndex == self.lineRanges.count - 1; | |||||
} | |||||
- (unichar)previousCharacter | |||||
{ | |||||
if (self.atBeginningOfLine) | |||||
return '\0'; | |||||
return [self.string characterAtIndex:self.location - 1]; | |||||
} | |||||
- (unichar)nextCharacter | |||||
{ | |||||
if (self.atEndOfLine) | |||||
return '\n'; | |||||
return [self.string characterAtIndex:self.location]; | |||||
} | |||||
- (NSString *)previousWord | |||||
{ | |||||
return [self previousWordWithCharactersFromSet:NSCharacterSet.alphanumericCharacterSet]; | |||||
} | |||||
- (NSString *)nextWord | |||||
{ | |||||
return [self nextWordWithCharactersFromSet:NSCharacterSet.alphanumericCharacterSet]; | |||||
} | |||||
- (NSString *)previousWordWithCharactersFromSet:(NSCharacterSet *)set | |||||
{ | |||||
NSUInteger start = MAX(self.currentLineRange.location, self.startLocation); | |||||
NSUInteger end = self.currentRange.location; | |||||
NSRange range = NSMakeRange(start, end-start); | |||||
NSRange result = [self.string rangeOfCharacterFromSet:set.invertedSet | |||||
options:NSBackwardsSearch | |||||
range:range]; | |||||
if (result.location == NSNotFound) | |||||
return [self.string substringWithRange:range]; | |||||
NSUInteger wordLocation = NSMaxRange(result); | |||||
NSRange wordRange = NSMakeRange(wordLocation, end-wordLocation); | |||||
return [self.string substringWithRange:wordRange]; | |||||
} | |||||
- (NSString *)nextWordWithCharactersFromSet:(NSCharacterSet *)set | |||||
{ | |||||
NSRange result = [self.string rangeOfCharacterFromSet:set.invertedSet | |||||
options:0 | |||||
range:self.currentRange]; | |||||
if (result.location == NSNotFound) | |||||
return [self.string substringWithRange:self.currentRange]; | |||||
NSRange wordRange = self.currentRange; | |||||
wordRange.length = result.location - wordRange.location; | |||||
return [self.string substringWithRange:wordRange]; | |||||
} | |||||
- (void)advance | |||||
{ | |||||
if (self.atEndOfLine) | |||||
return; | |||||
self.location += 1; | |||||
} | |||||
- (void)advanceToNextLine | |||||
{ | |||||
// If at the last line, just go to the end of the line | |||||
if (self.rangeIndex == self.lineRanges.count - 1) | |||||
{ | |||||
self.location = NSMaxRange(self.currentLineRange); | |||||
} | |||||
// Otherwise actually go to the next line | |||||
else | |||||
{ | |||||
self.rangeIndex += 1; | |||||
self.currentRange = self.currentLineRange; | |||||
} | |||||
} | |||||
- (BOOL)matchString:(NSString *)string | |||||
{ | |||||
if (self.currentRange.length < string.length) | |||||
return NO; | |||||
NSUInteger location = self.location; | |||||
for (NSUInteger idx=0; idx<string.length; idx++) | |||||
{ | |||||
if ([string characterAtIndex:idx] != [self.string characterAtIndex:location+idx]) | |||||
return NO; | |||||
} | |||||
self.location += string.length; | |||||
return YES; | |||||
} | |||||
- (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet | |||||
{ | |||||
NSRange searchRange = self.currentRange; | |||||
NSRange range = [self.string rangeOfCharacterFromSet:[aSet invertedSet] | |||||
options:0 | |||||
range:searchRange]; | |||||
NSUInteger current = self.location; | |||||
if (range.location == NSNotFound) | |||||
{ | |||||
self.currentRange = NSMakeRange(NSMaxRange(self.currentRange), 0); | |||||
} | |||||
else | |||||
{ | |||||
self.currentRange = NSMakeRange(range.location, NSMaxRange(self.currentRange)-range.location); | |||||
} | |||||
return self.location - current; | |||||
} | |||||
- (NSUInteger)skipCharactersFromSet:(NSCharacterSet *)aSet max:(NSUInteger)maxToSkip | |||||
{ | |||||
NSUInteger idx=0; | |||||
for (; idx<maxToSkip; idx++) | |||||
{ | |||||
unichar character = [self nextCharacter]; | |||||
if ([aSet characterIsMember:character]) | |||||
[self advance]; | |||||
else | |||||
break; | |||||
} | |||||
return idx; | |||||
} | |||||
- (NSUInteger)skipEmptyLines | |||||
{ | |||||
NSUInteger skipped = 0; | |||||
while (![self atEndOfString]) | |||||
{ | |||||
[self beginTransaction]; | |||||
[self skipWhitespace]; | |||||
if (!self.atEndOfLine) | |||||
{ | |||||
[self commitTransaction:NO]; | |||||
break; | |||||
} | |||||
[self commitTransaction:YES]; | |||||
[self advanceToNextLine]; | |||||
skipped++; | |||||
} | |||||
return skipped; | |||||
} | |||||
- (NSUInteger)skipIndentationUpTo:(NSUInteger)maxSpacesToSkip | |||||
{ | |||||
NSUInteger skipped = 0; | |||||
[self beginTransaction]; | |||||
while (!self.atEndOfLine && skipped < maxSpacesToSkip) | |||||
{ | |||||
unichar character = self.nextCharacter; | |||||
if (character == ' ') | |||||
skipped += 1; | |||||
else if (character == '\t') | |||||
{ | |||||
skipped -= skipped % 4; | |||||
skipped += 4; | |||||
} | |||||
else | |||||
break; | |||||
[self advance]; | |||||
} | |||||
[self commitTransaction:skipped <= maxSpacesToSkip]; | |||||
return skipped; | |||||
} | |||||
- (NSUInteger)skipNestedBracketsWithDelimiter:(unichar)delimiter | |||||
{ | |||||
NSString *delimiters = __delimitersForCharacter(delimiter); | |||||
if (delimiters == nil) | |||||
return 0; | |||||
unichar openDelimiter = [delimiters characterAtIndex:0]; | |||||
unichar closeDelimeter = [delimiters characterAtIndex:1]; | |||||
if ([self nextCharacter] != openDelimiter) | |||||
return 0; | |||||
[self beginTransaction]; | |||||
NSUInteger location = self.location; | |||||
[self advance]; | |||||
NSString *specialChars = [NSString stringWithFormat:@"%@\\", delimiters]; | |||||
NSCharacterSet *boringChars = [[NSCharacterSet characterSetWithCharactersInString:specialChars] invertedSet]; | |||||
NSUInteger nestingLevel = 1; | |||||
while (nestingLevel > 0) | |||||
{ | |||||
if (self.atEndOfLine) | |||||
{ | |||||
[self commitTransaction:NO]; | |||||
return 0; | |||||
} | |||||
[self skipCharactersFromSet:boringChars]; | |||||
unichar nextChar = self.nextCharacter; | |||||
[self advance]; | |||||
if (nextChar == openDelimiter) | |||||
{ | |||||
nestingLevel++; | |||||
} | |||||
else if (nextChar == closeDelimeter) | |||||
{ | |||||
nestingLevel--; | |||||
} | |||||
else if (nextChar == '\\') | |||||
{ | |||||
// skip a second character after a backslash | |||||
[self advance]; | |||||
} | |||||
} | |||||
[self commitTransaction:YES]; | |||||
return self.location - location; | |||||
} | |||||
- (NSUInteger)skipToEndOfLine | |||||
{ | |||||
NSUInteger length = self.currentRange.length; | |||||
self.location = NSMaxRange(self.currentRange); | |||||
return length; | |||||
} | |||||
- (NSUInteger)skipToLastCharacterOfLine | |||||
{ | |||||
NSUInteger length = self.currentRange.length - 1; | |||||
self.location = NSMaxRange(self.currentRange) - 1; | |||||
return length; | |||||
} | |||||
- (NSUInteger)skipWhitespace | |||||
{ | |||||
return [self skipCharactersFromSet:NSCharacterSet.whitespaceCharacterSet]; | |||||
} | |||||
- (NSUInteger)skipWhitespaceAndNewlines | |||||
{ | |||||
NSCharacterSet *whitespaceSet = NSCharacterSet.whitespaceCharacterSet; | |||||
NSUInteger length = 0; | |||||
while (!self.atEndOfString) | |||||
{ | |||||
if (self.atEndOfLine) | |||||
{ | |||||
[self advanceToNextLine]; | |||||
length++; | |||||
} | |||||
else | |||||
{ | |||||
NSUInteger spaces = [self skipCharactersFromSet:whitespaceSet]; | |||||
if (spaces == 0) | |||||
break; | |||||
length += spaces; | |||||
} | |||||
} | |||||
return length; | |||||
} | |||||
#pragma mark - Public Properties | |||||
- (NSUInteger)location | |||||
{ | |||||
return self.currentRange.location; | |||||
} | |||||
- (void)setLocation:(NSUInteger)location | |||||
{ | |||||
// If the new location isn't a part of the current range, then find the range it belongs to. | |||||
if (!NSLocationInRange(location, self.currentLineRange)) | |||||
{ | |||||
__block NSUInteger index = 0; | |||||
[self.lineRanges enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop){ | |||||
NSRange range = [obj rangeValue]; | |||||
*stop = NSLocationInRange(location, range) || location == NSMaxRange(range); | |||||
index = idx; | |||||
}]; | |||||
self.rangeIndex = index; | |||||
} | |||||
self.currentRange = NSMakeRange(location, NSMaxRange(self.currentLineRange)-location); | |||||
} | |||||
#pragma mark - Private Methods | |||||
- (NSArray *)_lineRangesForString:(NSString *)aString | |||||
{ | |||||
NSMutableArray *result = [NSMutableArray array]; | |||||
NSUInteger location = 0; | |||||
NSUInteger idx; | |||||
for (idx=0; idx<aString.length; idx++) | |||||
{ | |||||
unichar character = [aString characterAtIndex:idx]; | |||||
if (character == '\r' || character == '\n') | |||||
{ | |||||
NSRange range = NSMakeRange(location, idx-location); | |||||
[result addObject:[NSValue valueWithRange:range]]; | |||||
// If it's a carriage return, check for a line feed too | |||||
if (character == '\r') | |||||
{ | |||||
if (idx + 1 < aString.length && [aString characterAtIndex:idx + 1] == '\n') | |||||
{ | |||||
idx += 1; | |||||
} | |||||
} | |||||
location = idx + 1; | |||||
} | |||||
} | |||||
// Add the final line if the string doesn't end with a newline | |||||
if (location < aString.length) | |||||
{ | |||||
NSRange range = NSMakeRange(location, aString.length-location); | |||||
[result addObject:[NSValue valueWithRange:range]]; | |||||
} | |||||
return result; | |||||
} | |||||
- (NSUInteger)_locationOfCharacter:(unichar)character inRange:(NSRange)range | |||||
{ | |||||
NSString *characterString = [NSString stringWithCharacters:&character length:1]; | |||||
NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:characterString]; | |||||
NSRange result = [self.string rangeOfCharacterFromSet:characterSet options:0 range:range]; | |||||
return result.location; | |||||
} | |||||
#pragma mark - Private Properties | |||||
- (NSRange)currentLineRange | |||||
{ | |||||
return [self.lineRanges[self.rangeIndex] rangeValue]; | |||||
} | |||||
@end |
// | |||||
// MMSpanParser.h | |||||
// MMMarkdown | |||||
// | |||||
// Copyright (c) 2012 Matt Diephouse. | |||||
// | |||||
// Permission is hereby granted, free of charge, to any person obtaining a copy | |||||
// of this software and associated documentation files (the "Software"), to deal | |||||
// in the Software without restriction, including without limitation the rights | |||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||||
// copies of the Software, and to permit persons to whom the Software is | |||||
// furnished to do so, subject to the following conditions: | |||||
// | |||||
// The above copyright notice and this permission notice shall be included in | |||||
// all copies or substantial portions of the Software. | |||||
// | |||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||||
// THE SOFTWARE. | |||||
// | |||||
#import <Foundation/Foundation.h> | |||||
#import "MMMarkdown.h" | |||||
#import "MMParser.h" | |||||
@class MMElement; | |||||
@class MMScanner; | |||||
NS_ASSUME_NONNULL_BEGIN | |||||
@interface MMSpanParser : NSObject | |||||
- (id)initWithExtensions:(MMMarkdownExtensions)extensions; | |||||
- (nullable NSArray *)parseSpansInBlockElement:(MMElement *)block withScanner:(MMScanner *)scanner; | |||||
- (nullable NSArray *)parseSpansInTableColumns:(NSArray *)columns withScanner:(MMScanner *)scanner; | |||||
@end | |||||
NS_ASSUME_NONNULL_END |