@@ -3,18 +3,18 @@ ARCHS = arm64 arm64e | |||
include $(THEOS)/makefiles/common.mk | |||
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 | |||
SUBPROJECTS += prefs | |||
include $(THEOS_MAKE_PATH)/aggregate.mk | |||
after-install:: | |||
install.exec "killall -9 Reddit" | |||
install.exec "killall -9 Apollo" | |||
install.exec "killall -9 narwhal" | |||
install.exec "killall -9 AlienBlue" |
@@ -54,11 +54,27 @@ | |||
<key>label</key> | |||
<string>Enable Narwhal</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>isAlienBlueEnabled</string> | |||
<key>label</key> | |||
<string>Enable Alien Blue</string> | |||
</dict> | |||
<dict> | |||
<key>cell</key> | |||
<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> | |||
<string>Apollo </string> | |||
<string>visibility</string> | |||
</dict> | |||
<dict> | |||
<key>PostNotification</key> | |||
@@ -72,7 +88,21 @@ | |||
<key>key</key> | |||
<string>isApolloDeletedCommentsOnly</string> | |||
<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> | |||
<key>cell</key> |
@@ -0,0 +1,60 @@ | |||
/* -- 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 |
@@ -0,0 +1,216 @@ | |||
#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); | |||
} | |||
} | |||
} | |||
@@ -0,0 +1,40 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,70 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,39 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,92 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,171 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,37 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,282 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,40 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,324 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,7 @@ | |||
// | |||
// Prefix header for all source files of the 'MMMarkdown' target in the 'MMMarkdown' project | |||
// | |||
#ifdef __OBJC__ | |||
#import <Foundation/Foundation.h> | |||
#endif |
@@ -0,0 +1,82 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,73 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,41 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,79 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,499 @@ | |||
// | |||
// 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 |
@@ -0,0 +1,45 @@ | |||
// | |||
// 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 |