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, ").
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.”