├── .gitignore ├── NSString+IATitlecase.h ├── NSTextView+IATitlecase.h ├── IATitlecaseRegularExpressionManager.h ├── Tests ├── Info.plist └── Tests.m ├── IATitlecaseRegularExpressionManager.m ├── LICENSE.md ├── IARegularExpressionManager.h ├── Titlecase.regex ├── NSTextView+IATitlecase.m ├── README.md ├── NSString+IATitlecase.m ├── IARegularExpressionManager.m └── Titlecase.xcodeproj └── project.pbxproj /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | xcuserdata 3 | -------------------------------------------------------------------------------- /NSString+IATitlecase.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+IATitlecase.h 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface NSString (IATitlecase) 13 | 14 | @property (readonly) NSString *titlecaseString; 15 | 16 | @end 17 | 18 | NS_ASSUME_NONNULL_END 19 | -------------------------------------------------------------------------------- /NSTextView+IATitlecase.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSTextView+IATitlecase.h 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | /// This category adds a “Make Title Case” transform to contextual menu in every NSTextView and NSTextField. 11 | @interface NSTextView (IATitlecase) 12 | 13 | - (IBAction)titlecaseWord:(nullable id)sender; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /IATitlecaseRegularExpressionManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // IATitlecaseRegularExpressionManager.h 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import "IARegularExpressionManager.h" 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | @interface IATitlecaseRegularExpressionManager : IARegularExpressionManager 13 | 14 | + (instancetype)sharedManager; 15 | 16 | @property (nonatomic) NSRegularExpression *titlecaseExpression; 17 | @property (nonatomic) NSRegularExpression *startExceptionExpression; 18 | @property (nonatomic) NSRegularExpression *endExceptionExpression; 19 | @property (nonatomic) NSRegularExpression *startHyphenatedCompoundExpression; 20 | @property (nonatomic) NSRegularExpression *endHyphenatedCompoundExpression; 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /IATitlecaseRegularExpressionManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // IATitlecaseRegularExpressionManager.m 3 | // Text 4 | // 5 | // Created by Anton Sotkov on 23.01.16. 6 | // Copyright © 2016 Information Architects Inc. All rights reserved. 7 | // 8 | 9 | #import "IATitlecaseRegularExpressionManager.h" 10 | 11 | @implementation IATitlecaseRegularExpressionManager 12 | 13 | + (instancetype)sharedManager { 14 | static IATitlecaseRegularExpressionManager *sharedManager; 15 | static dispatch_once_t onceToken; 16 | dispatch_once(&onceToken, ^{ 17 | sharedManager = [[IATitlecaseRegularExpressionManager alloc] initWithRegularExpressions:self.regularExpressions options:0]; 18 | }); 19 | return sharedManager; 20 | } 21 | 22 | + (NSString *)regularExpressions { 23 | NSURL *URL = [[NSBundle bundleForClass:self.class] URLForResource:@"Titlecase" withExtension:@"regex"]; 24 | NSData *data = [[NSData alloc] initWithContentsOfURL:URL]; 25 | NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 26 | return string; 27 | } 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2016 Information Architects Inc. 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. 10 | -------------------------------------------------------------------------------- /IARegularExpressionManager.h: -------------------------------------------------------------------------------- 1 | // 2 | // IARegularExpressionManager.h 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | 10 | NS_ASSUME_NONNULL_BEGIN 11 | 12 | /// Regular expressions benefit from proper formatting and comments just like any other code. Double escapes and the lack of multiple line support make inline regular expressions less portable and maintable. This class is intended to be used with strings initialized from a file where you can properly format regular expressions. 13 | /// 14 | /// \code 15 | /// imageExpression = { 16 | /// (?i) 17 | /// $nameExpression 18 | /// \.(jpe?g|png|gif) 19 | /// } 20 | /// nameExpression = { 21 | /// (\w+) 22 | /// } 23 | /// \endcode 24 | /// 25 | /// Expression names and variables must be at least 3 characters long. You should use expressive names. Variables must be separated by a space or enclosed in parentheses. 26 | /// 27 | /// \c IARegularExpressionManager is intended to be subclassed. Regular expressions in a string which have matching properties will be set using KVC. A shared instance is recommended to avoid parsing the file again and again. 28 | @interface IARegularExpressionManager : NSObject 29 | 30 | /// Will parse the regular expressions and attempt to set them using KVC. Options will be applied to each expression. 31 | - (instancetype)initWithRegularExpressions:(NSString *)regularExpressions options:(NSRegularExpressionOptions)options NS_DESIGNATED_INITIALIZER; 32 | 33 | /// Use designated intiializer. 34 | - (instancetype)init NS_UNAVAILABLE; 35 | 36 | @end 37 | 38 | NS_ASSUME_NONNULL_END 39 | -------------------------------------------------------------------------------- /Titlecase.regex: -------------------------------------------------------------------------------- 1 | smallWordExpression = { 2 | (? In-Flight 55 | startHyphenatedCompoundExpression = { 56 | (?i) 57 | \b 58 | # Negative lookbehind for a hyphen; we don't want to match man-in-the-middle but do want (in-flight) 59 | (? "Stand-In" (Stand is already capped at this point) 66 | endHyphenatedCompoundExpression = { 67 | (?i) 68 | \b 69 | # Negative lookbehind for a hyphen; we don't want to match man-in-the-middle but do want (stand-in) 70 | (? aspectInfo) { 18 | // __unsafe_unretained prevents ARC from overreleasing menu at the end. 19 | __unsafe_unretained NSMenu *menu; 20 | [[aspectInfo originalInvocation] getReturnValue:&menu]; 21 | for (NSMenuItem *menuItem in menu.itemArray.reverseObjectEnumerator) { 22 | if (menuItem.hasSubmenu == NO) { 23 | continue; 24 | } 25 | NSInteger capitalizeIndex = [menuItem.submenu indexOfItemWithTarget:nil andAction:@selector(capitalizeWord:)]; 26 | if (capitalizeIndex != -1) { 27 | NSMenuItem *capitalizeAsTitleItem = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"Make Title Case", @"Title case transform title.") action:@selector(titlecaseWord:) keyEquivalent:@""]; 28 | [menuItem.submenu addItem:capitalizeAsTitleItem]; 29 | break; 30 | } 31 | } 32 | } error:&error]; 33 | } 34 | 35 | - (void)titlecaseWord:(id)sender { 36 | if (self.isEditable == NO) { 37 | return; 38 | } 39 | [self selectWord:self]; 40 | NSArray *ranges = self.selectedRanges.copy; 41 | NSMutableArray *strings = [[NSMutableArray alloc] init]; 42 | for (NSValue *rangeValue in ranges) { 43 | NSString *text = [self.string substringWithRange:rangeValue.rangeValue]; 44 | NSString *titlecaseText = text.titlecaseString; 45 | [strings addObject:titlecaseText]; 46 | } 47 | if ([self shouldChangeTextInRanges:ranges replacementStrings:strings] == NO) { 48 | return; 49 | } 50 | for (NSInteger rangeIndex = 0; rangeIndex < ranges.count; rangeIndex++) { 51 | [self replaceCharactersInRange:ranges[rangeIndex].rangeValue withString:strings[rangeIndex]]; 52 | } 53 | [self setSelectedRanges:ranges]; 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Titlecase 2 | 3 | Modern Objective-C port of [title case script][Script] by John Gruber and Aristotle Pagaltzis. 4 | 5 | [John Gruber][Article]: 6 | 7 | > It’s pretty easy to write a non-clever title-casing function. The simplest way is to just capitalize the first letter of every word. That’s not right, though, because it’ll leave you with capitalized small words like if, in, of, on, etc. What you want is something that not only knows not to capitalize such words, but will un-capitalize them if they’re erroneously capitalized in the input. 8 | 9 | We originally developed the port for [iA Writer 3.1.1][iA Writer]. We decided to open source it because we think it woud be useful to others. We’d be happy to see proper title case available in more apps. 10 | 11 | ### Components 12 | 13 | #### `NSString+IATitlecase` 14 | 15 | `NSString` category which returns a copy of a given string transformed to title case. 16 | 17 | #### `IARegularExpressionManager` 18 | 19 | Regular expressions benefit from proper formatting and comments just like any other code. Double escapes and the lack of multiple line support make inline regular expressions less portable and maintable in Cocoa. This class is intended to be used with strings initialized from a file where you can properly format regular expressions. 20 | 21 | ``` 22 | imageExpression = { 23 | (?i) 24 | $nameExpression 25 | \.(jpe?g|png|gif) 26 | } 27 | nameExpression = { 28 | (\w+) 29 | } 30 | ``` 31 | 32 | Expression names and variables must be at least 3 characters long. You should use expressive names. Variables must be separated by a space or enclosed in parentheses. 33 | 34 | `IARegularExpressionManager` is intended to be subclassed. Regular expressions in a string which have matching properties will be set using KVC. A shared instance is recommended to avoid parsing the file again and again. 35 | 36 | #### `IATitlecaseRegularExpressionManager` and `Titlecase.regex` 37 | 38 | Regular expressions from [title case script][Script] by John Gruber and Aristotle Pagaltzis. This is a good example of a concrete subclass of `IARegularExpressionManager`. 39 | 40 | #### `NSTextView+IATitlecase` 41 | 42 | This category adds a “Make Title Case” transform to contextual menu in every `NSTextView` and `NSTextField` in your app. Requires [`Aspects`][Aspects] by Peter Steinberger. `titlecaseWord:` behavior closely follows system methods such as `lowercaseWord:` and `capitalizeWord: 43 | 44 | ### Tests 45 | 46 | Titlecase passes [tests][Tests] from John Gruber, David Gouch, and Aristotle Pagaltzis. Only trimming tests are excluded, because our `titlecaseString` implementation follows the behavior of system methods such as `uppercaseString` and `capitalizedString`. 47 | 48 | ### Supported SDK Versions 49 | 50 | Titlecase requires ARC. Tested with iOS 9 and OS X 10.11. Supports iOS 7+ and OS X 10.7+. 51 | 52 | ### License 53 | 54 | MIT license. Copyright © 2016 Information Architects Inc. 55 | 56 | [Article]: http://daringfireball.net/2008/05/title_case 57 | [Script]: https://gist.github.com/gruber/9f9e8650d68b13ce4d78 58 | [Aspects]: https://github.com/steipete/Aspects 59 | [Tests]: https://github.com/ap/titlecase/blob/master/test.pl 60 | [iA Writer]: http://ia.net/writer -------------------------------------------------------------------------------- /Tests/Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // IATitlecaseTests.m 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import 9 | #import "NSString+IATitlecase.h" 10 | 11 | @interface Tests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation Tests 16 | 17 | - (void)testTitlecase { 18 | // Test cases are from John Gruber, David Gouch, and Aristotle Pagaltzis. https://github.com/ap/titlecase/blob/master/test.pl 19 | NSDictionary *titlecaseStrings = @{ 20 | @"For step-by-step directions email someone@gmail.com": @"For Step-by-Step Directions Email someone@gmail.com", 21 | @"2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'": @"2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'", 22 | @"Have you read “The Lottery”?": @"Have You Read “The Lottery”?", 23 | @"your hair[cut] looks (nice)": @"Your Hair[cut] Looks (Nice)", 24 | @"People probably won't put http://foo.com/bar/ in titles": @"People Probably Won't Put http://foo.com/bar/ in Titles", 25 | @"Scott Moritz and TheStreet.com’s million iPhone la‑la land": @"Scott Moritz and TheStreet.com’s Million iPhone La‑La Land", 26 | @"BlackBerry vs. iPhone": @"BlackBerry vs. iPhone", 27 | @"Notes and observations regarding Apple’s announcements from ‘The Beat Goes On’ special event": @"Notes and Observations Regarding Apple’s Announcements From ‘The Beat Goes On’ Special Event", 28 | @"Read markdown_rules.txt to find out how _underscores around words_ will be interpretted": @"Read markdown_rules.txt to Find Out How _Underscores Around Words_ Will Be Interpretted", 29 | @"Q&A with Steve Jobs: 'That's what happens in technology'": @"Q&A With Steve Jobs: 'That's What Happens in Technology'", 30 | @"What is AT&T's problem?": @"What Is AT&T's Problem?", 31 | @"Apple deal with AT&T falls through": @"Apple Deal With AT&T Falls Through", 32 | @"this v that": @"This v That", 33 | @"this vs that": @"This vs That", 34 | @"this v. that": @"This v. That", 35 | @"this vs. that": @"This vs. That", 36 | @"The SEC's Apple probe: what you need to know": @"The SEC's Apple Probe: What You Need to Know", 37 | @"'by the way, small word at the start but within quotes.'": @"'By the Way, Small Word at the Start but Within Quotes.'", 38 | @"Small word at end is nothing to be afraid of": @"Small Word at End Is Nothing to Be Afraid Of", 39 | @"Starting sub-phrase with a small word: a trick, perhaps?": @"Starting Sub-Phrase With a Small Word: A Trick, Perhaps?", 40 | @"Sub-phrase with a small word in quotes: 'a trick, perhaps?'": @"Sub-Phrase With a Small Word in Quotes: 'A Trick, Perhaps?'", 41 | @"Sub-phrase with a small word in quotes: \"a trick, perhaps?\"": @"Sub-Phrase With a Small Word in Quotes: \"A Trick, Perhaps?\"", 42 | @"\"Nothing to Be Afraid of?\"": @"\"Nothing to Be Afraid Of?\"", 43 | @"a thing": @"A Thing", 44 | @"Dr. Strangelove (or: how I Learned to Stop Worrying and Love the Bomb)": @"Dr. Strangelove (Or: How I Learned to Stop Worrying and Love the Bomb)", 45 | @"IF IT’S ALL CAPS, FIX IT": @"If It’s All Caps, Fix It", 46 | @"What could/should be done about slashes?": @"What Could/Should Be Done About Slashes?", 47 | @"Never touch paths like /var/run before/after /boot": @"Never Touch Paths Like /var/run Before/After /boot", 48 | }; 49 | [titlecaseStrings enumerateKeysAndObjectsUsingBlock:^(NSString *text, NSString *expectedTitlecaseString, BOOL *stop) { 50 | XCTAssertEqualObjects(text.titlecaseString, expectedTitlecaseString); 51 | }]; 52 | } 53 | 54 | @end 55 | -------------------------------------------------------------------------------- /NSString+IATitlecase.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+IATitlecase.m 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import "NSString+IATitleCase.h" 9 | #import "IATitlecaseRegularExpressionManager.h" 10 | 11 | @interface NSString (IATitlecaseAdditions) 12 | 13 | @property (readonly) NSString *firstCharacterCapitalizedString; 14 | 15 | @end 16 | 17 | @implementation NSMutableString (IATitlecaseExpression) 18 | 19 | - (void)replaceMatchesForRegularExpression:(NSRegularExpression *)expression usingTransforms:(NSDictionary *)transforms { 20 | [expression enumerateMatchesInString:self options:0 range:NSMakeRange(0, self.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 21 | for (NSInteger matchRangeIndex = 0; matchRangeIndex < result.numberOfRanges; matchRangeIndex++) { 22 | NSRange range = [result rangeAtIndex:matchRangeIndex]; 23 | if (range.location == NSNotFound) { 24 | continue; 25 | } 26 | NSString *(^transform)(NSString *word, NSInteger *next) = transforms[@(matchRangeIndex)]; 27 | if (transform == nil) { 28 | continue; 29 | } 30 | NSString *substring = [self substringWithRange:range]; 31 | NSString *modifiedSubstring = transform(substring, &matchRangeIndex); 32 | if (substring.length != modifiedSubstring.length) { 33 | @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Modified string must have the same length as original." userInfo:@{}]; 34 | } 35 | [self replaceCharactersInRange:range withString:modifiedSubstring]; 36 | } 37 | }]; 38 | } 39 | 40 | @end 41 | 42 | #define transform(expression) ^NSString *(NSString *word, NSInteger *next){ expression } 43 | 44 | @implementation NSString (IATitlecase) 45 | 46 | - (NSString *)titlecaseString { 47 | // Regular expressions and their processing are based on the particular revision of Perl script with one difference: white space is not stripped, because I want the NSString category act similarly to -(uppercase|lowercase|capitalized)String. 48 | // https://gist.github.com/gruber/9f9e8650d68b13ce4d78/d7d64ccbc6e1c86b0aae5cb368ea1f6f7f3738c5 49 | IATitlecaseRegularExpressionManager *manager = [IATitlecaseRegularExpressionManager sharedManager]; 50 | NSMutableString *titlecaseString = [([self.uppercaseString isEqualToString:self] ? self.lowercaseString : self) mutableCopy]; 51 | [titlecaseString replaceMatchesForRegularExpression:manager.titlecaseExpression usingTransforms:@{ 52 | @1: transform( return word; ), 53 | @2: transform( *next = 6; return word; ), 54 | @3: transform( *next = 6; return word.lowercaseString; ), 55 | @4: transform( *next = 6; return word.lowercaseString.firstCharacterCapitalizedString; ), 56 | @5: transform( return word; ), 57 | @6: transform( return word; ), 58 | }]; 59 | [titlecaseString replaceMatchesForRegularExpression:manager.startExceptionExpression usingTransforms:@{ 60 | @1: transform( return word; ), 61 | @2: transform( return word.lowercaseString.firstCharacterCapitalizedString; ), 62 | }]; 63 | [titlecaseString replaceMatchesForRegularExpression:manager.endExceptionExpression usingTransforms:@{ 64 | @1: transform( return word.lowercaseString.firstCharacterCapitalizedString; ), 65 | }]; 66 | [titlecaseString replaceMatchesForRegularExpression:manager.startHyphenatedCompoundExpression usingTransforms:@{ 67 | @1: transform( return word.lowercaseString.firstCharacterCapitalizedString; ), 68 | }]; 69 | [titlecaseString replaceMatchesForRegularExpression:manager.endHyphenatedCompoundExpression usingTransforms:@{ 70 | @1: transform( return word; ), 71 | @2: transform( return word.firstCharacterCapitalizedString; ), 72 | }]; 73 | return titlecaseString; 74 | } 75 | 76 | - (NSString *)firstCharacterCapitalizedString { 77 | NSRange firstCharacterRange = [self rangeOfComposedCharacterSequenceAtIndex:0]; 78 | NSString *firstCharacter = [self substringWithRange:firstCharacterRange]; 79 | return [self stringByReplacingCharactersInRange:firstCharacterRange withString:firstCharacter.uppercaseString]; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /IARegularExpressionManager.m: -------------------------------------------------------------------------------- 1 | // 2 | // IARegularExpressionManager.m 3 | // Titlecase 4 | // 5 | // Copyright © 2016 Information Architects Inc. All rights reserved. 6 | // 7 | 8 | #import "IARegularExpressionManager.h" 9 | 10 | @implementation IARegularExpressionManager 11 | 12 | - (instancetype)initWithRegularExpressions:(NSString *)regularExpressions options:(NSRegularExpressionOptions)options { 13 | self = [super init]; 14 | NSDictionary *regularExpressionsDictionary = [self dictionaryWithRegularExpressionString:regularExpressions options:options]; 15 | [regularExpressionsDictionary enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *pattern, BOOL *stop) { 16 | if ([self respondsToSelector:NSSelectorFromString(propertyName)] == NO) { 17 | return; 18 | } 19 | if ([self valueForKey:propertyName]) { 20 | return; 21 | } 22 | NSError *regularExpressionError; 23 | NSRegularExpression *regularExpression = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionAllowCommentsAndWhitespace|options error:®ularExpressionError]; 24 | if (regularExpression == nil) { 25 | @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"Error in regular expression %@: %@.", propertyName, regularExpressionError.localizedDescription] userInfo:@{}]; 26 | } 27 | [self setValue:regularExpression forKey:propertyName]; 28 | }]; 29 | return self; 30 | } 31 | 32 | - (NSDictionary *)dictionaryWithRegularExpressionString:(NSString *)regularExpressionString options:(NSRegularExpressionOptions)options { 33 | NSMutableDictionary *regularExpressions = [[NSMutableDictionary alloc] init]; 34 | NSError *error; 35 | // Format expression parses the file. 36 | NSRegularExpression *expressionRegularExpression = [NSRegularExpression regularExpressionWithPattern:@"^([a-zA-Z0-9]{3,})\\ ?=\\ ?\\{\\n(.*?)\\n^\\}$" options:NSRegularExpressionDotMatchesLineSeparators|NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace|options error:&error]; 37 | [expressionRegularExpression enumerateMatchesInString:regularExpressionString options:NSMatchingWithoutAnchoringBounds range:NSMakeRange(0, regularExpressionString.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { 38 | if (result) { 39 | NSString *expressionName = [regularExpressionString substringWithRange:[result rangeAtIndex:1]]; 40 | NSString *expression = [regularExpressionString substringWithRange:[result rangeAtIndex:2]]; 41 | // Single-line expressions are trimmed (they are likely to be used as a variable). 42 | if ([expression containsString:@"\n"]) { 43 | expression = [expression stringByTrimmingCharactersInSet:[NSCharacterSet characterSetWithCharactersInString:@"\t "]]; 44 | } 45 | regularExpressions[expressionName] = expression; 46 | } 47 | }]; 48 | // Some of the expressions may be used as a part of other expressions, as variables. We “copy & paste” those expressions. 49 | NSRegularExpression *variableRegularExpression = [NSRegularExpression regularExpressionWithPattern:@"(?<= ^|\\ |\\( )\\$([a-zA-Z0-9]{3,})(?= $|\\ |\\) )" options:NSRegularExpressionDotMatchesLineSeparators|NSRegularExpressionAnchorsMatchLines|NSRegularExpressionAllowCommentsAndWhitespace|options error:&error]; 50 | [regularExpressions.copy enumerateKeysAndObjectsUsingBlock:^(NSString *name, NSString *expression, BOOL *stop) { 51 | NSArray *referenceMatches = [variableRegularExpression matchesInString:expression options:0 range:NSMakeRange(0, expression.length)]; 52 | NSMutableString *updatedExpression = expression.mutableCopy; 53 | for (NSTextCheckingResult *referenceMatch in referenceMatches.reverseObjectEnumerator) { 54 | NSString *referenceName = [expression substringWithRange:[referenceMatch rangeAtIndex:1]]; 55 | NSString *referenceExpression = regularExpressions[referenceName]; 56 | if (referenceExpression) { 57 | [updatedExpression replaceCharactersInRange:referenceMatch.range withString:referenceExpression]; 58 | } 59 | } 60 | regularExpressions[name] = updatedExpression; 61 | }]; 62 | return regularExpressions; 63 | } 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /Titlecase.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3A1BB13D1C995B8B000F9CA0 /* Tests.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A1BB13C1C995B8B000F9CA0 /* Tests.m */; }; 11 | 3A1BB14B1C995BC4000F9CA0 /* IARegularExpressionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A1BB1431C995BC4000F9CA0 /* IARegularExpressionManager.m */; }; 12 | 3A1BB14C1C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A1BB1451C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.m */; }; 13 | 3A1BB14D1C995BC4000F9CA0 /* NSString+IATitlecase.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A1BB1471C995BC4000F9CA0 /* NSString+IATitlecase.m */; }; 14 | 3A1BB14F1C995BC4000F9CA0 /* Titlecase.regex in Resources */ = {isa = PBXBuildFile; fileRef = 3A1BB14A1C995BC4000F9CA0 /* Titlecase.regex */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 3A1BB1391C995B8B000F9CA0 /* Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 19 | 3A1BB13C1C995B8B000F9CA0 /* Tests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Tests.m; sourceTree = ""; }; 20 | 3A1BB13E1C995B8B000F9CA0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 21 | 3A1BB1421C995BC4000F9CA0 /* IARegularExpressionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IARegularExpressionManager.h; sourceTree = ""; }; 22 | 3A1BB1431C995BC4000F9CA0 /* IARegularExpressionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IARegularExpressionManager.m; sourceTree = ""; }; 23 | 3A1BB1441C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = IATitlecaseRegularExpressionManager.h; sourceTree = ""; }; 24 | 3A1BB1451C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = IATitlecaseRegularExpressionManager.m; sourceTree = ""; }; 25 | 3A1BB1461C995BC4000F9CA0 /* NSString+IATitlecase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+IATitlecase.h"; sourceTree = ""; }; 26 | 3A1BB1471C995BC4000F9CA0 /* NSString+IATitlecase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+IATitlecase.m"; sourceTree = ""; }; 27 | 3A1BB1481C995BC4000F9CA0 /* NSTextView+IATitlecase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSTextView+IATitlecase.h"; sourceTree = ""; }; 28 | 3A1BB1491C995BC4000F9CA0 /* NSTextView+IATitlecase.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSTextView+IATitlecase.m"; sourceTree = ""; }; 29 | 3A1BB14A1C995BC4000F9CA0 /* Titlecase.regex */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = Titlecase.regex; sourceTree = ""; }; 30 | 3A1BB1521C995CA3000F9CA0 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 31 | /* End PBXFileReference section */ 32 | 33 | /* Begin PBXFrameworksBuildPhase section */ 34 | 3A1BB1361C995B8B000F9CA0 /* Frameworks */ = { 35 | isa = PBXFrameworksBuildPhase; 36 | buildActionMask = 2147483647; 37 | files = ( 38 | ); 39 | runOnlyForDeploymentPostprocessing = 0; 40 | }; 41 | /* End PBXFrameworksBuildPhase section */ 42 | 43 | /* Begin PBXGroup section */ 44 | 3A1BB12E1C995B5A000F9CA0 = { 45 | isa = PBXGroup; 46 | children = ( 47 | 3A1BB1521C995CA3000F9CA0 /* README.md */, 48 | 3A1BB1501C995BCB000F9CA0 /* Titlecase */, 49 | 3A1BB13B1C995B8B000F9CA0 /* Tests */, 50 | 3A1BB13A1C995B8B000F9CA0 /* Products */, 51 | ); 52 | sourceTree = ""; 53 | }; 54 | 3A1BB13A1C995B8B000F9CA0 /* Products */ = { 55 | isa = PBXGroup; 56 | children = ( 57 | 3A1BB1391C995B8B000F9CA0 /* Tests.xctest */, 58 | ); 59 | name = Products; 60 | sourceTree = ""; 61 | }; 62 | 3A1BB13B1C995B8B000F9CA0 /* Tests */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | 3A1BB13C1C995B8B000F9CA0 /* Tests.m */, 66 | 3A1BB13E1C995B8B000F9CA0 /* Info.plist */, 67 | ); 68 | path = Tests; 69 | sourceTree = ""; 70 | }; 71 | 3A1BB1501C995BCB000F9CA0 /* Titlecase */ = { 72 | isa = PBXGroup; 73 | children = ( 74 | 3A1BB1461C995BC4000F9CA0 /* NSString+IATitlecase.h */, 75 | 3A1BB1471C995BC4000F9CA0 /* NSString+IATitlecase.m */, 76 | 3A1BB14A1C995BC4000F9CA0 /* Titlecase.regex */, 77 | 3A1BB1441C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.h */, 78 | 3A1BB1451C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.m */, 79 | 3A1BB1421C995BC4000F9CA0 /* IARegularExpressionManager.h */, 80 | 3A1BB1431C995BC4000F9CA0 /* IARegularExpressionManager.m */, 81 | 3A1BB1481C995BC4000F9CA0 /* NSTextView+IATitlecase.h */, 82 | 3A1BB1491C995BC4000F9CA0 /* NSTextView+IATitlecase.m */, 83 | ); 84 | name = Titlecase; 85 | sourceTree = ""; 86 | }; 87 | /* End PBXGroup section */ 88 | 89 | /* Begin PBXNativeTarget section */ 90 | 3A1BB1381C995B8B000F9CA0 /* Tests */ = { 91 | isa = PBXNativeTarget; 92 | buildConfigurationList = 3A1BB1411C995B8B000F9CA0 /* Build configuration list for PBXNativeTarget "Tests" */; 93 | buildPhases = ( 94 | 3A1BB1351C995B8B000F9CA0 /* Sources */, 95 | 3A1BB1361C995B8B000F9CA0 /* Frameworks */, 96 | 3A1BB1371C995B8B000F9CA0 /* Resources */, 97 | ); 98 | buildRules = ( 99 | ); 100 | dependencies = ( 101 | ); 102 | name = Tests; 103 | productName = Tests; 104 | productReference = 3A1BB1391C995B8B000F9CA0 /* Tests.xctest */; 105 | productType = "com.apple.product-type.bundle.unit-test"; 106 | }; 107 | /* End PBXNativeTarget section */ 108 | 109 | /* Begin PBXProject section */ 110 | 3A1BB12F1C995B5A000F9CA0 /* Project object */ = { 111 | isa = PBXProject; 112 | attributes = { 113 | LastUpgradeCheck = 0720; 114 | TargetAttributes = { 115 | 3A1BB1381C995B8B000F9CA0 = { 116 | CreatedOnToolsVersion = 7.2.1; 117 | }; 118 | }; 119 | }; 120 | buildConfigurationList = 3A1BB1321C995B5A000F9CA0 /* Build configuration list for PBXProject "Titlecase" */; 121 | compatibilityVersion = "Xcode 3.2"; 122 | developmentRegion = English; 123 | hasScannedForEncodings = 0; 124 | knownRegions = ( 125 | en, 126 | ); 127 | mainGroup = 3A1BB12E1C995B5A000F9CA0; 128 | productRefGroup = 3A1BB13A1C995B8B000F9CA0 /* Products */; 129 | projectDirPath = ""; 130 | projectRoot = ""; 131 | targets = ( 132 | 3A1BB1381C995B8B000F9CA0 /* Tests */, 133 | ); 134 | }; 135 | /* End PBXProject section */ 136 | 137 | /* Begin PBXResourcesBuildPhase section */ 138 | 3A1BB1371C995B8B000F9CA0 /* Resources */ = { 139 | isa = PBXResourcesBuildPhase; 140 | buildActionMask = 2147483647; 141 | files = ( 142 | 3A1BB14F1C995BC4000F9CA0 /* Titlecase.regex in Resources */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | /* End PBXResourcesBuildPhase section */ 147 | 148 | /* Begin PBXSourcesBuildPhase section */ 149 | 3A1BB1351C995B8B000F9CA0 /* Sources */ = { 150 | isa = PBXSourcesBuildPhase; 151 | buildActionMask = 2147483647; 152 | files = ( 153 | 3A1BB14D1C995BC4000F9CA0 /* NSString+IATitlecase.m in Sources */, 154 | 3A1BB14C1C995BC4000F9CA0 /* IATitlecaseRegularExpressionManager.m in Sources */, 155 | 3A1BB14B1C995BC4000F9CA0 /* IARegularExpressionManager.m in Sources */, 156 | 3A1BB13D1C995B8B000F9CA0 /* Tests.m in Sources */, 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXSourcesBuildPhase section */ 161 | 162 | /* Begin XCBuildConfiguration section */ 163 | 3A1BB1331C995B5A000F9CA0 /* Debug */ = { 164 | isa = XCBuildConfiguration; 165 | buildSettings = { 166 | }; 167 | name = Debug; 168 | }; 169 | 3A1BB1341C995B5A000F9CA0 /* Release */ = { 170 | isa = XCBuildConfiguration; 171 | buildSettings = { 172 | }; 173 | name = Release; 174 | }; 175 | 3A1BB13F1C995B8B000F9CA0 /* Debug */ = { 176 | isa = XCBuildConfiguration; 177 | buildSettings = { 178 | ALWAYS_SEARCH_USER_PATHS = NO; 179 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 180 | CLANG_CXX_LIBRARY = "libc++"; 181 | CLANG_ENABLE_MODULES = YES; 182 | CLANG_ENABLE_OBJC_ARC = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_CONSTANT_CONVERSION = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_EMPTY_BODY = YES; 187 | CLANG_WARN_ENUM_CONVERSION = YES; 188 | CLANG_WARN_INT_CONVERSION = YES; 189 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 190 | CLANG_WARN_UNREACHABLE_CODE = YES; 191 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 192 | CODE_SIGN_IDENTITY = "-"; 193 | COMBINE_HIDPI_IMAGES = YES; 194 | COPY_PHASE_STRIP = NO; 195 | DEBUG_INFORMATION_FORMAT = dwarf; 196 | ENABLE_STRICT_OBJC_MSGSEND = YES; 197 | ENABLE_TESTABILITY = YES; 198 | GCC_C_LANGUAGE_STANDARD = gnu99; 199 | GCC_DYNAMIC_NO_PIC = NO; 200 | GCC_NO_COMMON_BLOCKS = YES; 201 | GCC_OPTIMIZATION_LEVEL = 0; 202 | GCC_PREPROCESSOR_DEFINITIONS = ( 203 | "DEBUG=1", 204 | "$(inherited)", 205 | ); 206 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 207 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 208 | GCC_WARN_UNDECLARED_SELECTOR = YES; 209 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 210 | GCC_WARN_UNUSED_FUNCTION = YES; 211 | GCC_WARN_UNUSED_VARIABLE = YES; 212 | INFOPLIST_FILE = Tests/Info.plist; 213 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 214 | MACOSX_DEPLOYMENT_TARGET = 10.11; 215 | MTL_ENABLE_DEBUG_INFO = YES; 216 | ONLY_ACTIVE_ARCH = YES; 217 | PRODUCT_BUNDLE_IDENTIFIER = net.ia.Tests; 218 | PRODUCT_NAME = "$(TARGET_NAME)"; 219 | SDKROOT = macosx; 220 | }; 221 | name = Debug; 222 | }; 223 | 3A1BB1401C995B8B000F9CA0 /* Release */ = { 224 | isa = XCBuildConfiguration; 225 | buildSettings = { 226 | ALWAYS_SEARCH_USER_PATHS = NO; 227 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 228 | CLANG_CXX_LIBRARY = "libc++"; 229 | CLANG_ENABLE_MODULES = YES; 230 | CLANG_ENABLE_OBJC_ARC = YES; 231 | CLANG_WARN_BOOL_CONVERSION = YES; 232 | CLANG_WARN_CONSTANT_CONVERSION = YES; 233 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 234 | CLANG_WARN_EMPTY_BODY = YES; 235 | CLANG_WARN_ENUM_CONVERSION = YES; 236 | CLANG_WARN_INT_CONVERSION = YES; 237 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 238 | CLANG_WARN_UNREACHABLE_CODE = YES; 239 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 240 | CODE_SIGN_IDENTITY = "-"; 241 | COMBINE_HIDPI_IMAGES = YES; 242 | COPY_PHASE_STRIP = NO; 243 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 244 | ENABLE_NS_ASSERTIONS = NO; 245 | ENABLE_STRICT_OBJC_MSGSEND = YES; 246 | GCC_C_LANGUAGE_STANDARD = gnu99; 247 | GCC_NO_COMMON_BLOCKS = YES; 248 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 249 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 250 | GCC_WARN_UNDECLARED_SELECTOR = YES; 251 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 252 | GCC_WARN_UNUSED_FUNCTION = YES; 253 | GCC_WARN_UNUSED_VARIABLE = YES; 254 | INFOPLIST_FILE = Tests/Info.plist; 255 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; 256 | MACOSX_DEPLOYMENT_TARGET = 10.11; 257 | MTL_ENABLE_DEBUG_INFO = NO; 258 | PRODUCT_BUNDLE_IDENTIFIER = net.ia.Tests; 259 | PRODUCT_NAME = "$(TARGET_NAME)"; 260 | SDKROOT = macosx; 261 | }; 262 | name = Release; 263 | }; 264 | /* End XCBuildConfiguration section */ 265 | 266 | /* Begin XCConfigurationList section */ 267 | 3A1BB1321C995B5A000F9CA0 /* Build configuration list for PBXProject "Titlecase" */ = { 268 | isa = XCConfigurationList; 269 | buildConfigurations = ( 270 | 3A1BB1331C995B5A000F9CA0 /* Debug */, 271 | 3A1BB1341C995B5A000F9CA0 /* Release */, 272 | ); 273 | defaultConfigurationIsVisible = 0; 274 | defaultConfigurationName = Release; 275 | }; 276 | 3A1BB1411C995B8B000F9CA0 /* Build configuration list for PBXNativeTarget "Tests" */ = { 277 | isa = XCConfigurationList; 278 | buildConfigurations = ( 279 | 3A1BB13F1C995B8B000F9CA0 /* Debug */, 280 | 3A1BB1401C995B8B000F9CA0 /* Release */, 281 | ); 282 | defaultConfigurationIsVisible = 0; 283 | defaultConfigurationName = Release; 284 | }; 285 | /* End XCConfigurationList section */ 286 | }; 287 | rootObject = 3A1BB12F1C995B5A000F9CA0 /* Project object */; 288 | } 289 | --------------------------------------------------------------------------------