diff --git a/Default.tribo/Posts/2012-03-08-example-post.md b/Default.tribo/Posts/example-post/example-post.md similarity index 100% rename from Default.tribo/Posts/2012-03-08-example-post.md rename to Default.tribo/Posts/example-post/example-post.md diff --git a/Default.tribo/Posts/example-post/metadata.json b/Default.tribo/Posts/example-post/metadata.json new file mode 100644 index 0000000..74db01d --- /dev/null +++ b/Default.tribo/Posts/example-post/metadata.json @@ -0,0 +1 @@ +{"draft":true} \ No newline at end of file diff --git a/Mac App/Controllers/TBPostTableCellView.h b/Mac App/Controllers/TBPostTableCellView.h new file mode 100644 index 0000000..bbe0844 --- /dev/null +++ b/Mac App/Controllers/TBPostTableCellView.h @@ -0,0 +1,18 @@ +// +// TSPostTableCellView.h +// Tribo +// +// Created by Tanner Smith on 9/4/13. +// Copyright (c) 2013 Opt-6 Products, LLC. All rights reserved. +// + +#import + +@interface TBPostTableCellView : NSTableCellView + +@property (assign) IBOutlet NSTextField *title; +@property (assign) IBOutlet NSTextField *draft; +@property (assign) IBOutlet NSTextField *date; +@property (assign) IBOutlet NSTextField *postExcerpt; + +@end diff --git a/Mac App/Controllers/TBPostTableCellView.m b/Mac App/Controllers/TBPostTableCellView.m new file mode 100644 index 0000000..810f422 --- /dev/null +++ b/Mac App/Controllers/TBPostTableCellView.m @@ -0,0 +1,52 @@ +// +// TSPostTableCellView.m +// Tribo +// +// Created by Tanner Smith on 9/4/13. +// Copyright (c) 2013 Opt-6 Products, LLC. All rights reserved. +// + +#import "TBPostTableCellView.h" + +#import "NSTextField+TBAdditions.h" + +@implementation TBPostTableCellView + +@synthesize title, date, draft, postExcerpt; + +- (id)initWithFrame:(NSRect)frame +{ + self = [super initWithFrame:frame]; + if (self) { + // Initialization code here. + } + + return self; +} + +- (void)setBackgroundStyle:(NSBackgroundStyle)backgroundStyle { + switch (backgroundStyle) { + case NSBackgroundStyleDark: { + [title setTextColor:[NSColor whiteColor]]; + [date setTextColor:[NSColor whiteColor]]; + [draft setTextColor:[NSColor whiteColor]]; + [postExcerpt setTextColor:[NSColor whiteColor]]; + + [postExcerpt tb_setPlaceholderTextColor:[NSColor alternateSelectedControlColor]]; + + break; + } + default: { + [title setTextColor:[NSColor blackColor]]; + [date setTextColor:[NSColor alternateSelectedControlColor]]; + [draft setTextColor:[NSColor grayColor]]; + [postExcerpt setTextColor:[NSColor grayColor]]; + + [postExcerpt tb_setPlaceholderTextColor:[NSColor grayColor]]; + + break; + } + } +} + +@end diff --git a/Mac App/Controllers/TBPostsView.xib b/Mac App/Controllers/TBPostsView.xib index f2f1e78..d11885a 100644 --- a/Mac App/Controllers/TBPostsView.xib +++ b/Mac App/Controllers/TBPostsView.xib @@ -3,12 +3,12 @@ 1070 12E55 - 4488.1 + 3084 1187.39 626.00 com.apple.InterfaceBuilder.CocoaPlugin - 4488.1 + 3084 IBNSLayoutConstraint @@ -43,7 +43,7 @@ NSApplication - + 256 @@ -60,7 +60,6 @@ {410, 360} - _NS:1828 YES @@ -135,7 +134,7 @@ 3 2 - + 3 MQA @@ -163,7 +162,6 @@ {410, 360} - _NS:1826 @@ -175,7 +173,6 @@ -2147483392 {{224, 17}, {15, 102}} - _NS:1845 NO @@ -188,8 +185,6 @@ -2147483392 {{1, 119}, {223, 15}} - - _NS:1847 NO 1 @@ -200,7 +195,6 @@ {410, 360} - _NS:1824 133680 @@ -214,8 +208,6 @@ {410, 360} - - @@ -235,6 +227,14 @@ NSMenuMixedState + + + Draft + + 2147483647 + + + Preview in Safari @@ -276,6 +276,7 @@ title dateString markdownContent + draft TBPost YES @@ -329,6 +330,22 @@ 251 + + + draftMenuItem + + + + 713 + + + + draft: + + + + 714 + deleteSelectedRows: @@ -337,6 +354,14 @@ 405 + + + delegate + + + + 588 + contentArray: document.site.posts @@ -395,9 +420,9 @@ 266 - {{17, 66}, {270, 17}} + {{17, 66}, {178, 17}} - + _NS:78 {250, 750} YES @@ -422,28 +447,26 @@ NO - + 268 {{289, 66}, {101, 17}} - + _NS:3944 {750, 750} YES - + 68157504 71304256 - Selected - Date + Draft _NS:3944 - + - - 6 - System - highlightColor - + + 1 + MSAwIDAAA NO @@ -451,9 +474,9 @@ 268 - {{289, 66}, {101, 17}} + {{199, 66}, {191, 17}} - + _NS:3944 {750, 750} YES @@ -477,27 +500,6 @@ NO - - - 268 - {{17, 0}, {373, 63}} - - - _NS:360 - {250, 750} - YES - - 67108864 - 272630016 - Post excerpt - selected - - _NS:360 - - - - - NO - 268 @@ -537,40 +539,36 @@ 206 - - value: objectValue.dateString - - - - - - value: objectValue.dateString - value - objectValue.dateString - 2 - + + postExcerpt + + - 212 + 696 - - hidden: backgroundStyle - - - - - - hidden: backgroundStyle - hidden - backgroundStyle - - NSValueTransformerName - NSNegateBoolean - - 2 - + + date + + + + 697 + + + + draft + + + + 698 + + + + title + + - 213 + 699 @@ -593,7 +591,7 @@ hidden: backgroundStyle - + hidden: backgroundStyle @@ -602,7 +600,30 @@ 2 - 208 + 660 + + + + hidden2: objectValue.draft + + + + + + hidden2: objectValue.draft + hidden2 + objectValue.draft + + + + + + + + 2 + + + 661 @@ -642,59 +663,66 @@ - hidden: backgroundStyle - + value: objectValue.title + - + - hidden: backgroundStyle - hidden - backgroundStyle + value: objectValue.title + value + objectValue.title - NSValueTransformerName - NSNegateBoolean + NSNullPlaceholder + (no title) 2 - 214 + 410 - value: objectValue.markdownContent - + hidden: objectValue.draft + - - + + - value: objectValue.markdownContent - value - objectValue.markdownContent + hidden: objectValue.draft + hidden + objectValue.draft + + NSValueTransformerName + NSNegateBoolean + 2 - 215 + 523 - value: objectValue.title - + hidden2: backgroundStyle + - + - value: objectValue.title - value - objectValue.title - - NSNullPlaceholder - (no title) - + hidden2: backgroundStyle + hidden2 + backgroundStyle + + + + + + + 2 - 410 + 564 @@ -732,6 +760,7 @@ + Post Context Menu @@ -777,7 +806,6 @@ 9 40 3 - NO @@ -791,10 +819,9 @@ 1000 - 0 + 8 29 3 - NO @@ -808,10 +835,9 @@ 1000 - 0 + 8 29 3 - NO @@ -825,10 +851,9 @@ 1000 - 0 + 8 29 3 - NO @@ -875,45 +900,11 @@ 178 - - - 5 - 0 - - 6 - 1 - - 8 - - 1000 - - 6 - 24 - 3 - NO - - - - 6 - 0 - - 6 - 1 - - 20 - - 1000 - - 0 - 29 - 3 - NO - - - + + 11 0 - + 11 1 @@ -924,14 +915,13 @@ 6 24 2 - NO - - - 11 + + + 6 0 - - 11 + + 6 1 0.0 @@ -941,44 +931,25 @@ 6 24 2 - NO - - + + 6 0 - - 6 - 1 - - 20 - - 1000 - - 0 - 29 - 3 - NO - - - - 5 - 0 - + 6 1 - - 8 + + 0.0 1000 6 24 - 3 - NO + 2 - - + + 4 0 @@ -989,13 +960,12 @@ 1000 - 0 + 8 29 3 - NO - - + + 5 0 @@ -1006,33 +976,15 @@ 1000 - 0 + 8 29 3 - NO - - - 3 - 0 - - 3 - 1 - - 0.0 - - 1000 - - 6 - 24 - 2 - NO - - + 6 0 - + 6 1 @@ -1040,61 +992,41 @@ 1000 - 0 + 8 29 3 - NO - - - 4 + + + 11 0 - - 4 + + 11 1 0.0 1000 - 0 - 29 - 3 - NO + 6 + 24 + 2 - - + + 5 0 - - 5 - 1 - - 20 - - 1000 - - 0 - 29 - 3 - NO - - - - 6 - 0 - 6 + 5 1 - - 20 + + 0.0 1000 - 0 - 29 - 3 - NO + 6 + 24 + 2 @@ -1111,30 +1043,11 @@ 3 9 3 - NO - - - 5 - 0 - - 5 - 1 - - 20 - - 1000 - - 0 - 29 - 3 - NO - - - - + + @@ -1143,79 +1056,27 @@ - - 180 - - - - - 181 - - - - - 182 - - - - - 183 - - - - - - - - 184 - - - - - 185 - - - - - 186 - - - - - 187 - - - - - 188 - - - - - 189 - - - - - 190 - - - - - 191 - - - - - 192 - - - 193 + + + 7 + 0 + + 0 + 1 + + 185 + + 1000 + + 3 + 9 + 1 + @@ -1224,30 +1085,8 @@ - - - - - 195 - - - - - 196 - - - - - 197 - - - - - 198 - - - - + + 8 0 @@ -1257,13 +1096,11 @@ 63 1000 - + 3 9 1 - NO - @@ -1272,6 +1109,22 @@ + + + 7 + 0 + + 0 + 1 + + 172 + + 1000 + + 3 + 9 + 1 + @@ -1280,16 +1133,6 @@ - - 201 - - - - - 202 - - - 203 @@ -1300,11 +1143,6 @@ - - 205 - - - 376 @@ -1335,6 +1173,107 @@ + + 412 + + + + + + 7 + 0 + + 0 + 1 + + 95 + + 1000 + + 3 + 9 + 1 + + + + Static Text - Draft + + + 413 + + + Text Field Cell - Draft + + + 190 + + + + + 189 + + + + + 195 + + + + + 566 + + + + + 574 + + + + + 654 + + + + + 642 + + + + + 663 + + + + + 700 + + + + + 701 + + + + + 703 + + + + + 707 + + + + + 708 + + + + + 709 + + + @@ -1356,66 +1295,65 @@ - - + TBPostTableCellView + + + - - - - - - - - - - + + + com.apple.InterfaceBuilder.CocoaPlugin PostCell com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin + + + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - - + + - - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin + + + + + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin + com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin @@ -1430,20 +1368,96 @@ - 411 + 714 + + + + + TBPostsViewController + TBViewController + + id + id + id + id + id + + + + draft: + id + + + editPost: + id + + + previewPost: + id + + + revealPost: + id + + + unmarkDraft: + id + + + + NSMenuItem + NSTableView + + + + draftMenuItem + NSMenuItem + + + postTableView + NSTableView + + + + IBProjectSource + ./Classes/TBPostsViewController.h + + + + TBTableView + NSTableView + + deleteSelectedRows: + id + + + deleteSelectedRows: + + deleteSelectedRows: + id + + + + IBProjectSource + ./Classes/TBTableView.h + + + + TBViewController + NSViewController + + IBProjectSource + ./Classes/TBViewController.h + + + - 0 IBCocoaFramework - YES com.apple.InterfaceBuilder.CocoaPlugin.macosx - - com.apple.InterfaceBuilder.CocoaPlugin.macosx - - com.apple.InterfaceBuilder.CocoaPlugin.InterfaceBuilder3 diff --git a/Mac App/Controllers/TBPostsViewController.h b/Mac App/Controllers/TBPostsViewController.h index f40afb0..10af741 100644 --- a/Mac App/Controllers/TBPostsViewController.h +++ b/Mac App/Controllers/TBPostsViewController.h @@ -10,9 +10,13 @@ #import "TBViewController.h" #import -@interface TBPostsViewController : TBViewController +@interface TBPostsViewController : TBViewController @property (nonatomic, assign) IBOutlet NSTableView *postTableView; + +@property (assign) IBOutlet NSMenuItem *draftMenuItem; + - (IBAction)editPost:(id)sender; +- (IBAction)draft:(id)sender; - (IBAction)previewPost:(id)sender; - (IBAction)revealPost:(id)sender; @end diff --git a/Mac App/Controllers/TBPostsViewController.m b/Mac App/Controllers/TBPostsViewController.m index 9538122..26e3d23 100644 --- a/Mac App/Controllers/TBPostsViewController.m +++ b/Mac App/Controllers/TBPostsViewController.m @@ -23,6 +23,8 @@ - (void)undoMoveToTrashForURLs:(NSDictionary *)URLs; @implementation TBPostsViewController +@synthesize draftMenuItem; + #pragma mark - View Controller Configuration - (NSString *)defaultNibName { @@ -38,6 +40,30 @@ - (void)viewDidLoad { self.postTableView.doubleAction = @selector(editPost:); } +#pragma mark - NSMenuDelegate + +- (void)menuNeedsUpdate:(NSMenu *)menu { + TBSiteDocument *document = (TBSiteDocument *)self.document; + + NSInteger clickedRow = [self.postTableView clickedRow]; + + if (clickedRow < 0) { + [draftMenuItem setHidden:YES]; + + return; + } + + TBPost *clickedPost = (document.site.posts)[clickedRow]; + + [draftMenuItem setHidden:NO]; + + if ([clickedPost draft]) { + [draftMenuItem setState:NSOnState]; + } else { + [draftMenuItem setState:NSOffState]; + } +} + #pragma mark - Actions - (IBAction)editPost:(id)sender { @@ -46,6 +72,12 @@ - (IBAction)editPost:(id)sender { [[NSWorkspace sharedWorkspace] openURL:clickedPost.URL]; } +- (IBAction)draft:(id)sender { + TBPost *clickedPost = (self.document.site.posts)[[self.postTableView clickedRow]]; + + [clickedPost setDraft:![clickedPost draft]]; +} + - (IBAction)previewPost:(id)sender { TBPost *clickedPost = (self.document.site.posts)[[self.postTableView clickedRow]]; NSDateFormatter *formatter = [NSDateFormatter new]; diff --git a/Mac App/Controllers/TBSiteWindowController.m b/Mac App/Controllers/TBSiteWindowController.m index 382edf8..d0507b0 100644 --- a/Mac App/Controllers/TBSiteWindowController.m +++ b/Mac App/Controllers/TBSiteWindowController.m @@ -88,9 +88,14 @@ - (IBAction)showAddPostSheet:(id)sender { TBSiteDocument *document = (TBSiteDocument *)self.document; [self.addPostSheetController runModalForWindow:[document windowForSheet] completionBlock:^(NSString *title, NSString *slug) { NSError *error = nil; - NSURL *siteURL = [document.site addPostWithTitle:title slug:slug error:&error]; - if (!siteURL) + + TBPost *post = [TBPost postWithTitle:title slug:slug inSite:document.site error:&error]; + + if (post) { + [document.site addPost:post]; + } else { [self tb_presentErrorOnMainQueue:error]; + } }]; } diff --git a/Mac App/Publishing/TBFTPPublisher.m b/Mac App/Publishing/TBFTPPublisher.m index a049a48..b6e0135 100644 --- a/Mac App/Publishing/TBFTPPublisher.m +++ b/Mac App/Publishing/TBFTPPublisher.m @@ -22,7 +22,7 @@ @implementation TBFTPPublisher - (void)publish { self.site.published = YES; - [self.site process:nil]; + [self.site processIncludingDrafts:NO error:nil]; self.site.published = NO; NSString *hostname = (self.site.metadata)[TBSiteServerKey]; diff --git a/Mac App/Publishing/TBSFTPPublisher.m b/Mac App/Publishing/TBSFTPPublisher.m index 632a449..41e9af7 100644 --- a/Mac App/Publishing/TBSFTPPublisher.m +++ b/Mac App/Publishing/TBSFTPPublisher.m @@ -68,7 +68,7 @@ - (NSString *)passwordFromKeychain { - (void)publish { self.site.published = YES; - [self.site process:nil]; + [self.site processIncludingDrafts:NO error:nil]; self.site.published = NO; NSTask *rsync = [[NSTask alloc] init]; diff --git a/Mac App/TBSiteDocument.m b/Mac App/TBSiteDocument.m index d6086bb..0cef1fb 100644 --- a/Mac App/TBSiteDocument.m +++ b/Mac App/TBSiteDocument.m @@ -32,7 +32,7 @@ - (void)startPreview:(TBSiteDocumentPreviewCallback)callback { MAWeakSelfImport(); [[NSProcessInfo processInfo] disableSuddenTermination]; - if (![self.site process:&error]) { + if (![self.site processIncludingDrafts:YES error:&error]) { callback(nil, error); return; } @@ -95,7 +95,7 @@ - (void)reloadSite { NSError *error; [[NSProcessInfo processInfo] disableSuddenTermination]; - if (![self.site process:&error]) { + if (![self.site processIncludingDrafts:YES error:&error]) { [NSApp tb_presentErrorOnMainQueue:error]; return; } diff --git a/Shared/System Additions/NSTextField+TBAdditions.h b/Shared/System Additions/NSTextField+TBAdditions.h new file mode 100644 index 0000000..748c712 --- /dev/null +++ b/Shared/System Additions/NSTextField+TBAdditions.h @@ -0,0 +1,15 @@ +// +// NSTextField+TBAdditions.h +// Tribo +// +// Created by Tanner Smith on 9/5/13. +// Copyright (c) 2013 Opt-6 Products, LLC. All rights reserved. +// + +#import + +@interface NSTextField (TBAdditions) + +- (void)tb_setPlaceholderTextColor:(NSColor *)aColor; + +@end diff --git a/Shared/System Additions/NSTextField+TBAdditions.m b/Shared/System Additions/NSTextField+TBAdditions.m new file mode 100644 index 0000000..21e6972 --- /dev/null +++ b/Shared/System Additions/NSTextField+TBAdditions.m @@ -0,0 +1,29 @@ +// +// NSTextField+TBAdditions.m +// Tribo +// +// Created by Tanner Smith on 9/5/13. +// Copyright (c) 2013 Opt-6 Products, LLC. All rights reserved. +// + +#import "NSTextField+TBAdditions.h" + +@implementation NSTextField (TBAdditions) + +- (void)tb_setPlaceholderTextColor:(NSColor *)aColor { + NSString *placeholderString = [[self cell] placeholderString]; + + if (!placeholderString) { + placeholderString = [[[self cell] placeholderAttributedString] string]; + + if (!placeholderString) { + return; + } + } + + NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:placeholderString attributes:@{aColor : NSForegroundColorAttributeName}]; + + [[self cell] setPlaceholderAttributedString:attributedString]; +} + +@end diff --git a/Shared/TBPost.h b/Shared/TBPost.h index 20f86db..e8efe93 100644 --- a/Shared/TBPost.h +++ b/Shared/TBPost.h @@ -9,6 +9,8 @@ #import "TBPage.h" +#import "TBPostMetadata.h" + /*! @class TBPost @discussion A post represents a piece of writing, loaded from disk, with its @@ -28,9 +30,9 @@ @interface TBPost : TBPage /*! - Create a TBPost object from a file on-disk. + Create a TBPost object from a directory. @param URL - A filesystem URL pointing to the post file. + A filesystem URL pointing to the post directory. @param site The TBSite object which contains the post. @param error @@ -43,6 +45,22 @@ inSite:(TBSite *)site error:(NSError **)error; +/*! + Create a TBPost object with a title and slug. + @param title + The title of the post. + @param slug + The slug of the post. + @param site + The TBSite object which contains the post. + @param error + If the return value is nil, then this argument will contain an NSError + object describing what went wrong. + @return + A TBPost object, or nil if an error was encountered. + */ ++ (instancetype)postWithTitle:(NSString *)title slug:(NSString *)slug inSite:(TBSite *)site error:(NSError **)error; + /*! Parse the contents of the markdownContent property, saving the HTML output to the content property. @@ -61,7 +79,7 @@ The publishing date of the post. Derived from the filename of the post, which must begin with a calendar date in the form YYYY-MM-DD. */ -@property (nonatomic, strong) NSDate *date; +@property (nonatomic, strong, readonly) NSDate *date; /*! @property slug @@ -72,6 +90,26 @@ */ @property (nonatomic, strong) NSString *slug; +/*! + @property metadata + Any metadata that goes along with the post. Examaples include the post + date. + */ +@property (nonatomic, strong) TBPostMetadata *metadata; + +/*! + @property draft + The state of the post, i.e. is it a draft (unfinished). + */ +@property (nonatomic, assign) BOOL draft; + +/*! + @property postDirectory + The directory the post resides in. Contains the post markdown file (known + as slug.md) and metadata file. + */ +@property (nonatomic, strong) NSURL *postDirectory; + /*! @property markdownContent The original content of the post, before being converted to HTML by the diff --git a/Shared/TBPost.m b/Shared/TBPost.m index f72e739..7be54a1 100644 --- a/Shared/TBPost.m +++ b/Shared/TBPost.m @@ -17,15 +17,67 @@ @implementation TBPost + (instancetype)postWithURL:(NSURL *)URL inSite:(TBSite *)site error:(NSError **)error { - return (TBPost *)[super pageWithURL:URL inSite:site error:error]; + NSString *slug = [URL lastPathComponent]; + NSURL *postURL = [URL URLByAppendingPathComponent:[NSString stringWithFormat:@"%@.md", slug]]; + TBPost *post = [super pageWithURL:postURL inSite:site error:error]; + + if (post) { + post.postDirectory = URL; + post.slug = slug; + post.metadata = [TBPostMetadata metadataWithPostDirectory:URL withError:error]; + + if (post.metadata) { + return post; + } + } + + return nil; +} + ++ (instancetype)postWithTitle:(NSString *)title slug:(NSString *)slug inSite:(TBSite *)site error:(NSError **)error { + TBPost *post = [super new]; + + if (post) { + post.site = site; + + // Create the directory + NSString *filename = [NSString stringWithString:slug]; + + post.postDirectory = [site.postsDirectory URLByAppendingPathComponent:slug isDirectory:YES]; + + if (![[NSFileManager defaultManager] createDirectoryAtURL:post.postDirectory withIntermediateDirectories:YES attributes:nil error:error]) { + // Unable to create directory structure + return nil; + } + + // Metadata File + post.metadata = [TBPostMetadata metadataWithPostDirectory:post.postDirectory withError:error]; + [post.metadata writeWithError:error]; + + // Post File + NSURL *contentDestination = [[post.postDirectory URLByAppendingPathComponent:filename] URLByAppendingPathExtension:@"md"]; + + NSString *contents = [NSString stringWithFormat:@"# %@ #\n\n", title]; + + if (![contents writeToURL:contentDestination atomically:YES encoding:NSUTF8StringEncoding error:error]) { + return nil; + } + + post.URL = contentDestination; + [post parse:error]; + + return post; + } + + return nil; } - (BOOL)parse:(NSError **)error { - - [self loadMarkdownContent]; - - if (![self parseDateAndSlug:error]) + [self loadMarkdownContent]; + + if (![self parseSlug:error]) { return NO; + } [self parseTitle]; @@ -57,23 +109,16 @@ - (void)parseTitle { self.markdownContent = markdownContent; } -- (BOOL)parseDateAndSlug:(NSError **)error { - // Dates and slugs are parsed from a pattern in the post file name. - static NSRegularExpression *fileNameRegex; - if (fileNameRegex == nil) - fileNameRegex = [NSRegularExpression regularExpressionWithPattern:@"^(\\d+-\\d+-\\d+)-(.*)" options:0 error:nil]; - NSString *fileName = [self.URL.lastPathComponent stringByDeletingPathExtension]; - NSTextCheckingResult *fileNameResult = [fileNameRegex firstMatchInString:fileName options:0 range:NSMakeRange(0, fileName.length)]; - if (fileNameResult) { - NSDateFormatter *fileNameDateFormatter = [NSDateFormatter tb_cachedDateFormatterFromString:@"yyyy-MM-dd"]; - self.date = [fileNameDateFormatter dateFromString:[fileName substringWithRange:[fileNameResult rangeAtIndex:1]]]; - self.slug = [fileName substringWithRange:[fileNameResult rangeAtIndex:2]]; - } - else { - if (error) *error = TBError.badPostFileName(self.URL); - return NO; - } - return YES; +- (BOOL)parseSlug:(NSError **)error { + NSString *filename = [[self.URL lastPathComponent] stringByDeletingPathExtension]; + + if (filename && [filename length] > 0) { + self.slug = filename; + + return YES; + } + + return NO; } - (void)parseMarkdownContent { @@ -99,4 +144,31 @@ - (void)parseMarkdownContent { bufrelease(outputBuffer); } +- (NSDate *)date { + return [self.metadata publishedDate]; +} + +- (BOOL)draft { + return [self.metadata draft]; +} + +- (void)setDraft:(BOOL)draft { + [self.metadata setDraft:draft]; + + if (draft == NO) { + [self.metadata setPublishedDate:[NSDate date]]; + } else { + [self.metadata setPublishedDate:nil]; + } + + NSError *error = nil; + + [self.metadata writeWithError:&error]; + + if (error) { + [self.metadata setDraft:!draft]; + [self.metadata setPublishedDate:nil]; + } +} + @end diff --git a/Shared/TBPostMetadata.h b/Shared/TBPostMetadata.h new file mode 100644 index 0000000..f01d1f9 --- /dev/null +++ b/Shared/TBPostMetadata.h @@ -0,0 +1,93 @@ +// +// TSPostMetadata.h +// Tribo +// +// Created by Tanner Smith on 8/27/13. +// Copyright (c) 2013 The Tribo Authors. +// See the included License.md file. +// + +#import + +/*! + @class TSPostMetdata + @discussion Post metadata represents any extra data, i.e. metadata, about + a post (data other than the post itself). Metadata is written to the disk + in a standized format. + */ + +@interface TBPostMetadata : NSObject + +#define METADATA_FILENAME @"metadata.json" + +/*! + @property postDirectory + The directory the post resides in. Contains the post markdown file (known + as slug.md) and metadata file. + */ +@property (retain, strong) NSURL *postDirectory; + +/*! + @property path + The complete path to the metadata file. + */ +@property (retain, strong) NSURL *path; + +/*! + @property draft + The state of the post, i.e. is it a draft (unfinished). + */ +@property (assign) BOOL draft; + +/*! + @property publishedDate + The date when the post was published and was no longer a draft. + */ +@property (retain, strong) NSDate *publishedDate; + +/*! + Create an empty metadata object. + */ +- (instancetype)init; + +/*! + Create an metadata object from a post directory. + @param error + If the return value is nil, then this argument will contain an NSError + object describing what went wrong. + @return + A TSPostMetadata object, or nil if an error was encountered. + */ ++ (instancetype)metadataWithPostDirectory:(NSURL *)directory withError:(NSError **)error; + +/*! + Extract the metadata data from the given directory. + + Populates the member variables with this data. + + @param dictionary + Dictionary containing data. + */ +- (void)extractDataFromDictionary:(NSDictionary *)dictionary; + +/*! + Read the metadata file and store the data in the member variables. + @param error + If the return value is nil, then this argument will contain an NSError + object describing what went wrong. + @return + YES if an error was encountered. + */ +- (BOOL)readWithError:(NSError **)error; + +/*! + Write the metadata file from data in the member variables. + @param error + If the return value is nil, then this argument will contain an NSError + object describing what went wrong. + @return + YES if an error was encountered. + */ +- (BOOL)writeWithError:(NSError **)error; + +@end diff --git a/Shared/TBPostMetadata.m b/Shared/TBPostMetadata.m new file mode 100644 index 0000000..647d14c --- /dev/null +++ b/Shared/TBPostMetadata.m @@ -0,0 +1,107 @@ +// +// TSPostMetadata.m +// Tribo +// +// Created by Tanner Smith on 8/27/13. +// Copyright (c) 2013 The Tribo Authors. +// See the included License.md file. +// + +#import "TBPostMetadata.h" + +#import "NSDateFormatter+TBAdditions.h" + +@implementation TBPostMetadata + +@synthesize draft, publishedDate; +@synthesize postDirectory; + +- (instancetype)init { + if (self = [super init]) { + draft = YES; + publishedDate = nil; + } + + return self; +} + ++ (instancetype)metadataWithPostDirectory:(NSURL *)directory withError:(NSError **)error { + TBPostMetadata *metadata = [TBPostMetadata new]; + + if (metadata) { + metadata.postDirectory = directory; + metadata.path = [directory URLByAppendingPathComponent:METADATA_FILENAME]; + + [metadata readWithError:error]; + + return metadata; + } + + return nil; +} + ++ (instancetype)metadataWithDictionary:(NSDictionary *)dictionary { + TBPostMetadata *metadata = [TBPostMetadata new]; + + if (metadata) { + [metadata extractDataFromDictionary:dictionary]; + + return metadata; + } + + return nil; +} + +- (void)extractDataFromDictionary:(NSDictionary *)dictionary { + draft = [[dictionary objectForKey:@"draft"] boolValue]; + + // Must convert back into NSDate from string + NSDateFormatter *dateFormatter = [NSDateFormatter tb_cachedDateFormatterFromString:@"yyyy-MM-dd'T'hh:mm:ssZ"]; + NSString *dateString = [dictionary objectForKey:@"publishedDate"]; + + publishedDate = [dateFormatter dateFromString:dateString]; +} + +- (BOOL)readWithError:(NSError **)error { + if ([[NSFileManager defaultManager] fileExistsAtPath:[_path path]] == NO) { + return NO; + } + + NSData *data = [[NSData alloc] initWithContentsOfURL:_path]; + NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:error]; + + if (data) { + [self extractDataFromDictionary:dictionary]; + + return YES; + } + + return NO; +} + +- (BOOL)writeWithError:(NSError **)error { + if (!postDirectory) { + return NO; + } + + NSData *data = [NSJSONSerialization dataWithJSONObject:[self dictionary] options:0 error:error]; + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + return [string writeToURL:_path atomically:YES encoding:NSUTF8StringEncoding error:error]; +} + +- (NSDictionary *)dictionary { + NSMutableDictionary *dictionary = [NSMutableDictionary dictionary]; + + [dictionary setValue:[NSNumber numberWithBool:draft] forKey:@"draft"]; + + // Must format published date into string for JSON + NSDateFormatter *dateFormatter = [NSDateFormatter tb_cachedDateFormatterFromString:@"yyyy-MM-dd'T'hh:mm:ssZ"]; + NSString *dateString = [dateFormatter stringFromDate:publishedDate]; + + [dictionary setValue:dateString forKey:@"publishedDate"]; + + return dictionary; +} + +@end diff --git a/Shared/TBSite.h b/Shared/TBSite.h index 80d04ce..80f05c5 100644 --- a/Shared/TBSite.h +++ b/Shared/TBSite.h @@ -8,6 +8,7 @@ // #import "TBConstants.h" +#import "TBPost.h" @protocol TBSiteDelegate; @@ -38,13 +39,15 @@ /*! Process the entire site, writing the output into the destination directory. + @param includeDrafts + If set to YES, the generated sites will include any posts that are drafts. @param error If the return value is NO, then this argument will contain an NSError object describing what went wrong. @return YES on successful processing, NO if an error was encountered. */ -- (BOOL)process:(NSError **)error; +- (BOOL)processIncludingDrafts:(BOOL)includeDrafts error:(NSError **)error; /*! Parse all post files into TBPost objects. @@ -56,26 +59,7 @@ */ - (BOOL)parsePosts:(NSError **)error; -/*! - Add an empty post to the receiving site object. - @param title - The title of the post. Must not be nil. - @param slug - The slug of the post, i.e. the post title that will be used in URLs. - Must be URL-encoded and not nil. - @param error - If the return value is nil, then this argument will contain an NSError - object describing what went wrong. - @return - A filesystem URL pointing to the newly-created post file. - @discussion - The new post file has no content, but is pre-filled with a Markdown - title on the first line. The date of the post is automatically set to - today's date. - */ -- (NSURL *)addPostWithTitle:(NSString *)title - slug:(NSString *)slug - error:(NSError **)error; +- (void)addPost:(TBPost *)post; /*! @property root diff --git a/Shared/TBSite.m b/Shared/TBSite.m index 1bc5edb..d9901df 100644 --- a/Shared/TBSite.m +++ b/Shared/TBSite.m @@ -40,8 +40,7 @@ + (instancetype)siteWithRoot:(NSURL *)root { #pragma mark - Site Processing -- (BOOL)process:(NSError **)error { - +- (BOOL)processIncludingDrafts:(BOOL)includeDrafts error:(NSError **)error { if (![self loadRawDefaultTemplate:error]) return NO; @@ -51,7 +50,7 @@ - (BOOL)process:(NSError **)error { if (![self parsePosts:error]) return NO; - if (![self writePosts:error]) + if (![self writePostsIncludingDrafts:includeDrafts error:error]) return NO; if (![self writeFeed:error]) @@ -127,10 +126,12 @@ - (BOOL)parsePosts:(NSError **)error { } -- (BOOL)writePosts:(NSError **)error { - +- (BOOL)writePostsIncludingDrafts:(BOOL)includeDrafts error:(NSError **)error { for (TBPost *post in self.posts) { - + if (includeDrafts && post.draft) { + continue; + } + post.stylesheets = @[@{@"stylesheetName": @"post"}]; // Create the path to the folder where we are going to write the post file. @@ -301,18 +302,8 @@ - (NSString *)filteredContent:(NSString *)content fromFile:(NSURL *)file error:( #pragma mark - Site Modification -- (NSURL *)addPostWithTitle:(NSString *)title slug:(NSString *)slug error:(NSError **)error { - NSDate *currentDate = [NSDate date]; - NSDateFormatter *dateFormatter = [NSDateFormatter tb_cachedDateFormatterFromString:@"yyyy-MM-dd"]; - NSString *dateString = [dateFormatter stringFromDate:currentDate]; - NSString *filename = [NSString stringWithFormat:@"%@-%@", dateString, slug]; - NSURL *destination = [[self.postsDirectory URLByAppendingPathComponent:filename] URLByAppendingPathExtension:@"md"]; - NSString *contents = [NSString stringWithFormat:@"# %@ #\n\n", title]; - if (![contents writeToURL:destination atomically:YES encoding:NSUTF8StringEncoding error:error]) - return nil; - if (![self parsePosts:error]) - return nil; - return destination; +- (void)addPost:(TBPost *)post { + [self.posts addObject:post]; } - (void)setMetadata:(NSDictionary *)metadata { diff --git a/Shared/Template Additions/TBPost+TemplateAdditions.m b/Shared/Template Additions/TBPost+TemplateAdditions.m index ae863d8..7d62e2c 100644 --- a/Shared/Template Additions/TBPost+TemplateAdditions.m +++ b/Shared/Template Additions/TBPost+TemplateAdditions.m @@ -11,7 +11,7 @@ @implementation TBPost (TemplateAdditions) - (NSString *)dateString { - NSDateFormatter *dateStringFormatter = [NSDateFormatter tb_cachedDateFormatterFromString:@"d MMM yyyy"]; + NSDateFormatter *dateStringFormatter = [NSDateFormatter tb_cachedDateFormatterFromString:@"yyyy-MM-dd hh:mm a z"]; return [dateStringFormatter stringFromDate:self.date]; } - (NSString *)XMLDate { diff --git a/Tribo.xcodeproj/project.pbxproj b/Tribo.xcodeproj/project.pbxproj index a145f0c..787bfeb 100644 --- a/Tribo.xcodeproj/project.pbxproj +++ b/Tribo.xcodeproj/project.pbxproj @@ -163,6 +163,9 @@ 9B6B669D14F3546C00759DC3 /* TBAsset.m in Sources */ = {isa = PBXBuildFile; fileRef = 9B6B669B14F3546900759DC3 /* TBAsset.m */; }; 9BEF1C7A14F5CFCF0072B295 /* TBSourceViewControllerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BEF1C7814F5CFCF0072B295 /* TBSourceViewControllerViewController.m */; }; 9BEF1C7B14F5CFCF0072B295 /* TBSourceViewControllerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 9BEF1C7914F5CFCF0072B295 /* TBSourceViewControllerView.xib */; }; + C35C2A9717D8100800A6BE68 /* TBPostTableCellView.m in Sources */ = {isa = PBXBuildFile; fileRef = C35C2A9617D8100800A6BE68 /* TBPostTableCellView.m */; }; + C35C2A9A17D8B14B00A6BE68 /* NSTextField+TBAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = C35C2A9917D8B14B00A6BE68 /* NSTextField+TBAdditions.m */; }; + C3ACCD1817CD1EF00002CE67 /* TBPostMetadata.m in Sources */ = {isa = PBXBuildFile; fileRef = C3ACCD1717CD1EEF0002CE67 /* TBPostMetadata.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -381,6 +384,12 @@ 9BEF1C7714F5CFCF0072B295 /* TBSourceViewControllerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBSourceViewControllerViewController.h; sourceTree = ""; }; 9BEF1C7814F5CFCF0072B295 /* TBSourceViewControllerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBSourceViewControllerViewController.m; sourceTree = ""; }; 9BEF1C7914F5CFCF0072B295 /* TBSourceViewControllerView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = TBSourceViewControllerView.xib; sourceTree = ""; }; + C35C2A9517D8100800A6BE68 /* TBPostTableCellView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TBPostTableCellView.h; path = Controllers/TBPostTableCellView.h; sourceTree = ""; }; + C35C2A9617D8100800A6BE68 /* TBPostTableCellView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TBPostTableCellView.m; path = Controllers/TBPostTableCellView.m; sourceTree = ""; }; + C35C2A9817D8B14B00A6BE68 /* NSTextField+TBAdditions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSTextField+TBAdditions.h"; sourceTree = ""; }; + C35C2A9917D8B14B00A6BE68 /* NSTextField+TBAdditions.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTextField+TBAdditions.m"; sourceTree = ""; }; + C3ACCD1617CD1EEF0002CE67 /* TBPostMetadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TBPostMetadata.h; sourceTree = ""; }; + C3ACCD1717CD1EEF0002CE67 /* TBPostMetadata.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TBPostMetadata.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -458,6 +467,8 @@ 4A3DE5FD176CF16C00C20026 /* NSDateFormatter+TBAdditions.m */, 4AF2BFEB17B88AB6008D67AB /* NSResponder+TBAdditions.h */, 4AF2BFEC17B88AB6008D67AB /* NSResponder+TBAdditions.m */, + C35C2A9817D8B14B00A6BE68 /* NSTextField+TBAdditions.h */, + C35C2A9917D8B14B00A6BE68 /* NSTextField+TBAdditions.m */, ); path = "System Additions"; sourceTree = ""; @@ -580,6 +591,8 @@ 4A00337A1430216500F6702F /* TBSite.m */, 4A00337C143028CA00F6702F /* TBPost.h */, 4A00337D143028CA00F6702F /* TBPost.m */, + C3ACCD1617CD1EEF0002CE67 /* TBPostMetadata.h */, + C3ACCD1717CD1EEF0002CE67 /* TBPostMetadata.m */, 4A72F14014368E1200A164E9 /* TBPage.h */, 4A72F14114368E1200A164E9 /* TBPage.m */, 9B6B669A14F3546900759DC3 /* TBAsset.h */, @@ -874,6 +887,8 @@ isa = PBXGroup; children = ( 4AAC8E5E1456616F00AE978A /* TBPostsView.xib */, + C35C2A9517D8100800A6BE68 /* TBPostTableCellView.h */, + C35C2A9617D8100800A6BE68 /* TBPostTableCellView.m */, 4AAC8E5B1456609300AE978A /* TBPostsViewController.h */, 4AAC8E5C1456609300AE978A /* TBPostsViewController.m */, 4ABD6A5014417DBE007E2F5D /* TBQLPreviewView.xib */, @@ -1163,6 +1178,9 @@ 4A99C42917B4091D0023C9A9 /* CZAFileWatcher.m in Sources */, 4AA86A6C173EF28200046691 /* TBSite+TemplateAdditions.m in Sources */, 4AA86A70174124C300046691 /* TBPost+TemplateAdditions.m in Sources */, + C3ACCD1817CD1EF00002CE67 /* TBPostMetadata.m in Sources */, + C35C2A9717D8100800A6BE68 /* TBPostTableCellView.m in Sources */, + C35C2A9A17D8B14B00A6BE68 /* NSTextField+TBAdditions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; };