TweetFollow Us on Twitter

Jan 02 MacOSX

Volume Number: 18 (2002)
Issue Number: 01
Column Tag: Mac OS X

Smart Quotes

by ©2001 Andrew C. Stone. All Rights Reserved.

Adding automated curly quotes to Cocoa's Text system

Cocoa's Text System architecture is open, open-ended, and contains hooks for easy behavior modification. Because Cocoa Text is a unicode based system, it is by default international and lets your applications work correctly in a variety of scripts and languages. This article will teach you how to inspect the user's typed stream of keys, and convert straight quotes to open and closed curly quotes as the user types:

"Love is ‘THE' answer," she said.

Remembering all the modifier keys to create a curly quote is troublesome for end users, so we'll add the functionality to replace the boring straight quotes with fancy curly ones as they type. We'll also include a mechanism to "toggle through" the various options for those odd border cases, such as words beginning with a quote: 'Tis.

The Unicode

Cocoa's Text system relies on Unicode which allows each character in most standard font to be mapped to a unique short integer, a "unichar" defined in NSString.h:

typedef unsigned short unichar;

For example, each glyph in the Symbol font and Dingbats font are represented by unicode characters. The HTML standard also allows use of unicode characters, and here are the Unicode and HTML codes (which correspond to the unicode value) for curly quotes:

Character Unicode HTML Name How to type in manually
" 8220 “ double open quote Option-[
' 8216 ‘ single open quote Option-]
" 8221 ” double close Option-Shift-[
' 8217 ’ single close Option-Shift-]

Now that we know what will replace the straight quotes, we need a strategy for determining if a quote should be open or closed! It turns out that a very simple heuristic will work:

if character typed is a Straight Quote then

If it's the first character or is preceded by whitespace, turn it into Open Curly Quote

       else turn it into Close Curly Quote

So, when a newline, tab or space precedes a quote, it's probably an open quote. Note that this fails with ‘Tis! So, let's refine our heuristic to include a mechanism to "move through" the options straight, open and closed if the user has the quote in question solely selected:

if character typed is ‘ or " then

if selection contains a  single quote of any style,  replace with the next style in a loop (straight -> 
open -> closed -> straight and so on)

    else

If it's the first character or is preceded by whitespace, turn into Open Quote (single or double)

       else turn it into Close Quote

We have one more feature to add - the user should be able to DIRECTLY type in a straight quote when fancy quotes are enabled. We'll let them do this by changing any typed fancy quote to its straight counterpart (see How to type in manually in table above).

The Hook

One aspect of Cocoa that reigns supreme is the ease in which functionality can added, as well as the many different approaches that can be taken to accomplish the same task.

Where you put code depends on your architectural design. While you can always override a class, it's often easier to simply look at the "Delegate Methods" defined in various classes. These methods are optional: if an object, such as a text view, has a delegate, and that delegate implements one of these methods, then that method will be called at the appropriate time by the text view. If you overrode "keyDown" in NSTextView in a subclass, your text wouldn't swap the quotes on pasted text, since pasting does not call keyDown! So, instead we'll use this NSTextView delegate method that gets called anytime text is inserted either via keys or paste:

- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange 
replacementString:(NSString *)replacementString;

    // Delegate only.  If characters are changing, replacementString is what will replace the 
    affectedCharRange.  If attributes only are changing, replacementString will be nil.

This delegate method will work for us since we are only replacing single characters with another single character. If I had designed this method, instead of returning a boolean, I would have had this method return the actual new string to be inserted. Working with the method the way it is, we'll simply brute force stick in the character of our choice and return NO.

The Code

// Code is always more readable if you can turn the magic numbers into English!
#define SINGLE_QUOTE      ‘\''
#define SINGLE_OPEN_QUOTE   8216
#define SINGLE_CLOSE_QUOTE   8217
#define DOUBLE_QUOTE      ‘"‘
#define DOUBLE_OPEN_QUOTE   8220
#define DOUBLE_CLOSE_QUOTE   8221

// Smart Quote support - in our NSTextView's delegate class, we implement this method
// Be sure to connect the TextView's delegate outlet to your custom delegate class in Interface Builder
// or use  [myTextView setDelegate:self]; in your delegate class so this gets called:

- (BOOL)textView:(NSTextView *)textView shouldChangeTextInRange:(NSRange)affectedCharRange 
replacementString:(NSString *)replacementString;

    // Delegate only.  If characters are changing, replacementString is what will replace the 
    affectedCharRange.  If attributes only are changing, replacementString will be nil.
{

   // if we're just changing text attributes then we don't enter our processing loop
   // Also, provide a defaults mechanism to turn this fancy quoting off:
   
    if (replacementString && [[NSUserDefaults standardUserDefaults]boolForKey:SmartQuotes]) {
    
       // This is what is in our text object before anything is added:
        NSString *text = [[textView textStorage] string];
   
      // We want to know if we are at the very first character:
        unsigned int textLength = [text length];
   
      // how much are  we actually adding at this time:
        unsigned int i, length = [replacementString length];
        unichar c;
   
      // Should we change the string, we'll use this mutable string to hold the new values:
      // If it's non-nil when we get done, that means we've got work to do!
        NSMutableString *s = nil;
            
       // First, our toggle through mechanism:
        // special case: one char typed OVER a smart quote -> toggle them around 3 way
        // plain -> open -> closed -> plain
        // First deal with the case where they want another type of quote or a plain quote!
        if (length == 1 &&  affectedCharRange.length == 1) {
   
            if (((c = [text characterAtIndex:affectedCharRange.location])==DOUBLE_OPEN_QUOTE) || 
            (c == SINGLE_OPEN_QUOTE)) {
                // they had an open quote -> make it a closed one
                s = [NSString stringWithFormat:@"%C", c == DOUBLE_OPEN_QUOTE ? 
                DOUBLE_CLOSE_QUOTE : SINGLE_CLOSE_QUOTE];
      
            } else if (c == DOUBLE_CLOSE_QUOTE || c == SINGLE_CLOSE_QUOTE) {
                // they had a closed quote -> make it plain
                s = [NSString stringWithFormat:@"%C", c == DOUBLE_CLOSE_QUOTE  ? 
                DOUBLE_QUOTE : SINGLE_QUOTE];
      
            } else if (c == SINGLE_QUOTE || c == DOUBLE_QUOTE) {
             // they had a straight quote -> make it open
                 s = [NSString stringWithFormat:@"%C", c == SINGLE_QUOTE  ?  
                 SINGLE_OPEN_QUOTE : DOUBLE_OPEN_QUOTE];
           }
        } else {
   
        // otherwise go through replacement string one by one - paste can put in many characters at 
        one time!
            NSCharacterSet *startSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
            for (i = 0; i < length; i++) {
       
                unichar theChar = [replacementString characterAtIndex:i];
                unichar previousChar;
                
            // Find out the character which preceeds this one - context is everything!
                if(i == 0) {
                    if (affectedCharRange.location == 0 || textLength==0) previousChar = 0; 
                    // first char
                    else previousChar = [text characterAtIndex:affectedCharRange.location - 1];
                } else previousChar = [replacementString characterAtIndex:i-1];
                
            // When we encounter a straight quote, we decide whether it should be open or closed:
                if ((theChar == SINGLE_QUOTE) || (theChar == DOUBLE_QUOTE)){
      
               // lazily allocate the mutable string if we find something interesting
                    if (!s) s = [NSMutableString stringWithString:replacementString]; 
                    
                    if (previousChar == 0 || [startSet characterIsMember:previousChar] || 
                    (previousChar == DOUBLE_OPEN_QUOTE && theChar == SINGLE_QUOTE) || 
                    (previousChar == SINGLE_OPEN_QUOTE && theChar == DOUBLE_QUOTE))
                        c =  (theChar  == SINGLE_QUOTE  ?  SINGLE_OPEN_QUOTE : DOUBLE_OPEN_QUOTE);
                    else c =  (theChar  == SINGLE_QUOTE  ?  SINGLE_CLOSE_QUOTE : DOUBLE_CLOSE_QUOTE);
                    
                    [s replaceCharactersInRange:NSMakeRange(i,1) 
                    withString:[NSString stringWithFormat:@"%C", c]];
          
                } else if ((i==0) && (length == 1)) {
      
                    // we don't want to do this unless they are typing - paste may contain curly's 
                    already!
                    // reverse the meaning - they want to type in a plain one from the keyboard 
                    directly:
          
                    if ((theChar == SINGLE_CLOSE_QUOTE) || (theChar == SINGLE_OPEN_QUOTE)) {
                            s = [NSMutableString stringWithFormat:@"%C", SINGLE_QUOTE];
                    } else if ((theChar == DOUBLE_CLOSE_QUOTE) || (theChar == DOUBLE_OPEN_QUOTE)) {
                            s = [NSMutableString stringWithFormat:@"%C", DOUBLE_QUOTE];
                    }
                }
                        
            }
        }
        if (s) {
                // We'll be responsible for inserting the changed text ourselves
            // ideally, this method would return the string desired, but it doesn't
            // so we'll just pop the changes in directly ourselves:
                [[textView textStorage] replaceCharactersInRange:affectedCharRange withString:s];
                return NO;               
        }
    }
    // Otherwise, let the text system insert the text as typed...
    return YES;
}

Conclusion

The Cocoa Text system is easy to use and very extensible in a straightforward manner. An improvement you might consider for this smart quoting technique is to allow for internationalization. For example, in Germany, the open quote is along the text baseline, and other languages might use ‹› or other characters for quoting. To accomplish this, you would use NSLocalizedStringFromTable() and create an entry in the .strings file for each of the quotes in the languages that you support - refer to my article on Localization in MacTech a few months back for more information.


Andrew Stone, CEO of Stone Design, www.stone.com, has been coding in Cocoa as an independent software developer for over 13 years.

 
AAPL
$117.60
Apple Inc.
-1.03
MSFT
$47.47
Microsoft Corpora
-0.12
GOOG
$541.08
Google Inc.
+1.81

MacTech Search:
Community Search:

Software Updates via MacUpdate

MacUpdate Desktop 6.0.3 - Discover and i...
MacUpdate Desktop 6 brings seamless 1-click installs and version updates to your Mac. With a free MacUpdate account and MacUpdate Desktop 6, Mac users can now install almost any Mac app on macupdate.... Read more
SteerMouse 4.2.2 - Powerful third-party...
SteerMouse is an advanced driver for USB and Bluetooth mice. It also supports Apple Mighty Mouse very well. SteerMouse can assign various functions to buttons that Apple's software does not allow,... Read more
iMazing 1.1 - Complete iOS device manage...
iMazing (was DiskAid) is the ultimate iOS device manager with capabilities far beyond what iTunes offers. With iMazing and your iOS device (iPhone, iPad, or iPod), you can: Copy music to and from... Read more
PopChar X 7.0 - Floating window shows av...
PopChar X helps you get the most out of your font collection. With its crystal-clear interface, PopChar X provides a frustration-free way to access any font's special characters. Expanded... Read more
Carbon Copy Cloner 4.0.3 - Easy-to-use b...
Carbon Copy Cloner backups are better than ordinary backups. Suppose the unthinkable happens while you're under deadline to finish a project: your Mac is unresponsive and all you hear is an ominous,... Read more
ForeverSave 2.1.3 - Universal auto-save...
ForeverSave auto-saves all documents you're working on while simultaneously doing backup versioning in the background. Lost data can be quickly restored at any time. Losing data, caused by... Read more
Voila 3.8.1 - Capture, annotate, organiz...
Voila is a screen-capture, recording, and annotation tool that is a full-featured replacement for Mac's screen-capture and screen-recording capabilities. It has a large and robust set of editing,... Read more
SyncTwoFolders 2.0.6 - Syncs two user-sp...
SyncTwoFolders simply synchronizes two folders. It supports synchronization across mounted network drives and it is a possibility to run a simulation showing in a log what will be done. Please visit... Read more
Duplicate Annihilator 5.1.1 - Find and d...
Duplicate Annihilator takes on the time-consuming task of comparing the images in your iPhoto library using effective algorithms to make sure that no duplicate escapes. Duplicate Annihilator detects... Read more
HandBrake 0.10.0 - Versatile video encod...
HandBrake is a tool for converting video from nearly any format to a selection of modern, widely supported codecs. Supported Sources: VIDEO_TS folder, DVD image or real DVD (unencrypted -- CSS is... Read more

Latest Forum Discussions

See All

Screeny (Utilities)
Screeny 1.0 Device: iOS iPhone Category: Utilities Price: $.99, Version: 1.0 (iTunes) Description: Screeny is an utility app that helps you save space consumed by screenshots. It screens your camera roll and helps you to filter and... | Read more »
Tilt to Live Bundle Set to Arrive This T...
Tilt to Live Bundle Set to Arrive This Thanksgiving Posted by Ellis Spice on November 25th, 2014 [ permalink ] One Man Left has unveiled an upcoming Tilt to Live bundle, allowing players to get the series for a di | Read more »
BattleLore: Command (Entertainment)
BattleLore: Command 1.0 Device: iOS Universal Category: Entertainment Price: $9.99, Version: 1.0 (iTunes) Description: ***NOTE: Compatible with iPad 2/iPad mini, iPod touch 5 and up and iPhone 4S and up – WILL NOT RUN ON EARLIER... | Read more »
Weather Or Not Review
Weather Or Not Review By Jennifer Allen on November 25th, 2014 Our Rating: :: STYLISH WEATHER REPORTINGiPhone App - Designed for the iPhone, compatible with the iPad Check the weather quickly and conveniently with Weather or Not... | Read more »
The All-New Football Manager Handheld 20...
The All-New Football Manager Handheld 2015 is Available Now Posted by Jessica Fisher on November 25th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
Six iOS Games to Get You Ready for Thank...
Image Source: Friends Wiki At this point in the month, you or at least a few people you know are probably getting ready to scramble around (or are already scrambling around) for Thanksgiving Dinner. It’s a hectic day of precise oven utilization, but... | Read more »
Call of Duty: Heroes: Tips, Tricks, and...
Hello Heroes: What’d we think of Call of Duty‘s take on Clash of Clans? Check out our Call of Duty: Heroes review to find out! Just downloaded Call of Duty: Heroes and need some handy tips and tricks on how to get ahead of the rest? As we often do,... | Read more »
Call of Duty: Heroes Review
Call of Duty: Heroes Review By Jennifer Allen on November 25th, 2014 Our Rating: :: CLASH OF FRANCHISESUniversal App - Designed for iPhone and iPad Mix Clash of Clans with Call of Duty, and this is what you get.   | Read more »
Slider Review
Slider Review By Jordan Minor on November 25th, 2014 Our Rating: :: SLIDE TO PLAYUniversal App - Designed for iPhone and iPad Slider has all the excitement of unlocking your phone screen.   | Read more »
oh my giraffe (Games)
oh my giraffe 1.0.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0.0 (iTunes) Description: Eat fruits while being chased by lions. Cut the vines to send fruit plummeting onto the lions. Don't worry, your flexible... | Read more »

Price Scanner via MacPrices.net

Early Black Friday MacBook Pro sale: 15-inch...
 Best Buy has posted early Black Friday prices on 15″ Retina MacBook Pros, with models on sale for $300 off MSRP on their online store for a limited time. Choose free local store pickup (if available... Read more
A9 Chips Already?
It’s barely more than a couple of months since Apple got the first A8 systems-on-chip into consumer hands, but rumor and news focus is already turning to the next-generation A9 SoC. Apple Daily... Read more
NewerTech Announces NuGuard KXs Impact X-Orbi...
NewerTech has announced updates to its family of Impact X-Orbing Screen Armor bringing military grade, triple layer protection to Apple’s new iPhone 6 and 6 Plus. Like all models in the NuGuard KXs... Read more
13-inch 1.4GHz MacBook Air on sale for $889,...
 B&H Photo has the 13″ 1.4GHz/128GB MacBook Air on sale for $889 including free shipping plus NY tax only. Their price is $110 off MSRP. B&H will also include free copies of Parallels Desktop... Read more
Save up to $300 on Macs and iPads with your A...
Purchase a new Mac or iPad at The Apple Store for Education and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free,... Read more
Apple refurbished Mac Pros available for up t...
The Apple Store is offering Apple Certified Refurbished Mac Pros for up to $600 off the cost of new models. An Apple one-year warranty is included with each Mac Pro, and shipping is free. The... Read more
Jumptuit Launches One-Tap Windows 8.1 iTunes...
Jumptuit has launched Windows 8.1 support for One-Tap iTunes Sync. with which Windows 8.1 users can now easily sync their iTunes libraries with Microsoft OneDrive. Jumptuit provides easy access from... Read more
Apple restocks refurbished 13-inch 2014 Retin...
The Apple Store has restocked Apple Certified Refurbished 2014 13″ 2.6GHz Retina MacBook Pros for up to $230 off the cost of new models. An Apple one-year warranty is included with each model, and... Read more
CEA Study Finds More People Recycling Electro...
A new study by the Consumer Electronics Association (CEA) finds that electronics recycling receives the continued and growing support of consumers. According to the CEA,s Recycling and Reuse Study,... Read more
15″ 2.2GHz Retina MacBook Pro on sale for $17...
 B&H Photo has the 2014 15″ 2.2GHz Retina MacBook Pro on sale today for $1749. Shipping is free, and B&H charges NY sales tax only. B&H will also include free copies of Parallels Desktop... Read more

Jobs Board

*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Senior Event Manager, *Apple* Retail Market...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global event strategy. Delivering an overarching brand story; in-store, Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
*Apple* Solutions Consultant (ASC) - Apple (...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.