Smart Quotes (" ' -> “ ” ‘ ’)

Overview

“Smart Quotes” is the automatic replacement of the correct typographic quote character (‘ or ’ and “ or ”) as you type (' and "). It does not refer to the curved quotes themselves.

History

In the early days of Macintosh, you used to have to remember some arcane keyboard combinations to enter curved quotes. One day, while driving from Santa Barbara to a user group in southern California, I realized that this could be done automatically.

I first implemented smart quotes in the desk accessory miniWRITER, and then in Acta. I don’t know the exact date, but the earliest relevant change comment in miniWRITER is a bug fix in version 1.05, on 24 August 1986. So smart quotes are probably almost 20 years old.

I originally offered the algorithm to anyone who asked, provided they sent me a copy of the application it appeared in. I know PageMaker used it, as did WriteNow. Other applications have reverse-engineered the process. Unfortunately, they seldom offer a way to enter a straight quote (or inch mark, ").

Summary

As the user types, the characters typed are automatically replaced, according to the context of the insertion point, before being inserted into the text. A quote is turned into its left equivalent if it is at the beginning of the text, or if it follows a space, tab, return, or left punctuation (‘(,’ ‘[,’ ‘{,’ or ‘<’). a quote is also considered an opening quote if it follows an opening quote of the opposite type (as in: “‘sorry’ is all you have to say?” she asked).

Cocoa Implementation

Subclass NSTextView and override keyDown:.
unichar	gLeftApostrophe = 0x2018;
unichar	gRightApostrophe = 0x2019;

unichar	gLeftQuote = 0x201C;
unichar gRightQuote = 0x201D;

- (void) keyDown: (NSEvent*) anEvent
{
// Don't worry about having to allocate an NSString; [NSTextView keyDown:] will do so anyway,
// and it's apparently lazily instantiated by NSEvent.
NSString*	unmodifiedKeys = [anEvent charactersIgnoringModifiers];
NSString*	newKeys;

// Grab the first character, so we don't have to send messages to test for all the possibilities
// NOTE: In some cases, we get more than one character at once, if someone is banging on
// the keys or something. It might be better to iterate through unmodifiedKeys.
unichar		theChar = [unmodifiedKeys length] > 0 ? [unmodifiedKeys characterAtIndex: 0] : 0;
unichar		prevChar;

if (theChar == '"' || theChar == '\'') {
	// Possible smart quote/apostrophe
	if (![[NSUserDefaults standardUserDefaults] boolForKey: @"smartQuotes"]) {
		[super keyDown: anEvent];
		return;
	}
	if ([anEvent modifierFlags] & NSControlKeyMask) {
		// Override smart quotes with ctrl key; we will need to strip the modifier
		newKeys = [NSString stringWithCharacters: &theChar length: 1];
	} else {
		NSCharacterSet*	startSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
		NSRange			selection = [self selectedRange];
		if (selection.location == 0)
			prevChar = 0;
		else
			prevChar = [[self string] characterAtIndex: selection.location - 1];
		if (prevChar == 0 ||							// Beginning of text
			prevChar == '(' || prevChar == '[' || prevChar == '{' ||	// Left thingies
			prevChar == '<' || prevchar == 0x00AB ||			// More left thingies
			prevChar == 0x3008 || prevChar == 0x300A ||			// Even more left thingies (we could add more Unicode)
			(prevChar == gLeftQuote && theChar == '\'') ||			// Nest apostrophe inside quote
			(prevChar == gLeftApostrophe && theChar == '"') ||		// Alternate nesting
			[startSet characterIsMember: prevChar])				// Beginning of word/line
			newKeys = [NSString stringWithCharacters: theChar == '"' ? &gLeftQuote : &gLeftApostrophe length: 1];
		else
			newKeys = [NSString stringWithCharacters: theChar == '"' ? &gRightQuote : &gRightApostrophe length: 1];
	}
	NSEvent *newEvent = [NSEvent keyEventWithType: [anEvent type]
				location: [anEvent locationInWindow]
				modifierFlags: 0
				timestamp:1
				windowNumber:[[NSApp mainWindow] windowNumber]
				context:[NSGraphicsContext currentContext]
				characters:newKeys
				charactersIgnoringModifiers:newKeys
				isARepeat:NO
				keyCode: 0];
	[super keyDown: newEvent];
} else {
	[super keyDown: anEvent];
}
} // keyDown:
Thanks to Justin Bur for pointing out a bug (fixed above): “Many non-U.S. keyboards have floating accent keys, available without any modifiers, which generate a keyDown event with no characters.”
Copyright ©2006. Last updated 15 Aug 06 drd

David Dunham Page