First of all, I found this: Objective C HTML escape/unescape, but it doesn\'t work for me.
My encoded characters (come from a RSS feed, btw) look like this: &a
Those are called Character Entity References. When they take the form of &#<number>;
they are called numeric entity references. Basically, it's a string representation of the byte that should be substituted. In the case of &
, it represents the character with the value of 38 in the ISO-8859-1 character encoding scheme, which is &
.
The reason the ampersand has to be encoded in RSS is it's a reserved special character.
What you need to do is parse the string and replace the entities with a byte matching the value between &#
and ;
. I don't know of any great ways to do this in objective C, but this stack overflow question might be of some help.
Edit: Since answering this some two years ago there are some great solutions; see @Michael Waterfall's answer below.
Actually the great MWFeedParser framework of Michael Waterfall (referred to his answer) has been forked by rmchaara who has update it with ARC support!
You can find it in Github here
It really works great, I used stringByDecodingHTMLEntities method and works flawlessly.
I ought to post this on GitHub or something. This goes in a category of NSString, uses NSScanner
for the implementation, and handles both hex and decimal numeric character entities as well as the usual symbolic ones.
Also, it handles malformed strings (when you have an & followed by an invalid sequence of characters) relatively gracefully, which turned out to be crucial in my released app that uses this code.
- (NSString *)stringByDecodingXMLEntities {
NSUInteger myLength = [self length];
NSUInteger ampIndex = [self rangeOfString:@"&" options:NSLiteralSearch].location;
// Short-circuit if there are no ampersands.
if (ampIndex == NSNotFound) {
return self;
}
// Make result string with some extra capacity.
NSMutableString *result = [NSMutableString stringWithCapacity:(myLength * 1.25)];
// First iteration doesn't need to scan to & since we did that already, but for code simplicity's sake we'll do it again with the scanner.
NSScanner *scanner = [NSScanner scannerWithString:self];
do {
// Scan up to the next entity or the end of the string.
NSString *nonEntityString;
if ([scanner scanUpToString:@"&" intoString:&nonEntityString]) {
[result appendString:nonEntityString];
}
if ([scanner isAtEnd]) {
goto finish;
}
// Scan either a HTML or numeric character entity reference.
if ([scanner scanString:@"&" intoString:NULL])
[result appendString:@"&"];
else if ([scanner scanString:@"'" intoString:NULL])
[result appendString:@"'"];
else if ([scanner scanString:@""" intoString:NULL])
[result appendString:@"\""];
else if ([scanner scanString:@"<" intoString:NULL])
[result appendString:@"<"];
else if ([scanner scanString:@">" intoString:NULL])
[result appendString:@">"];
else if ([scanner scanString:@"&#" intoString:NULL]) {
BOOL gotNumber;
unsigned charCode;
NSString *xForHex = @"";
// Is it hex or decimal?
if ([scanner scanString:@"x" intoString:&xForHex]) {
gotNumber = [scanner scanHexInt:&charCode];
}
else {
gotNumber = [scanner scanInt:(int*)&charCode];
}
if (gotNumber) {
[result appendFormat:@"%C", charCode];
}
else {
NSString *unknownEntity = @"";
[scanner scanUpToString:@";" intoString:&unknownEntity];
[result appendFormat:@"&#%@%@;", xForHex, unknownEntity];
NSLog(@"Expected numeric character entity but got &#%@%@;", xForHex, unknownEntity);
}
[scanner scanString:@";" intoString:NULL];
}
else {
NSString *unknownEntity = @"";
[scanner scanUpToString:@";" intoString:&unknownEntity];
NSString *semicolon = @"";
[scanner scanString:@";" intoString:&semicolon];
[result appendFormat:@"%@%@", unknownEntity, semicolon];
NSLog(@"Unsupported XML character entity %@%@", unknownEntity, semicolon);
}
}
while (![scanner isAtEnd]);
finish:
return result;
}
This is the way I do it using RegexKitLite framework:
-(NSString*) decodeHtmlUnicodeCharacters: (NSString*) html {
NSString* result = [html copy];
NSArray* matches = [result arrayOfCaptureComponentsMatchedByRegex: @"\\&#([\\d]+);"];
if (![matches count])
return result;
for (int i=0; i<[matches count]; i++) {
NSArray* array = [matches objectAtIndex: i];
NSString* charCode = [array objectAtIndex: 1];
int code = [charCode intValue];
NSString* character = [NSString stringWithFormat:@"%C", code];
result = [result stringByReplacingOccurrencesOfString: [array objectAtIndex: 0]
withString: character];
}
return result;
}
Hope this will help someone.
As of iOS 7, you can decode HTML characters natively by using an NSAttributedString
with the NSHTMLTextDocumentType
attribute:
NSString *htmlString = @" & & < > ™ © ♥ ♣ ♠ ♦";
NSData *stringData = [htmlString dataUsingEncoding:NSUTF8StringEncoding];
NSDictionary *options = @{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType};
NSAttributedString *decodedString;
decodedString = [[NSAttributedString alloc] initWithData:stringData
options:options
documentAttributes:NULL
error:NULL];
The decoded attributed string will now be displayed as: & & < > ™ © ♥ ♣ ♠ ♦.
Note: This will only work if called on the main thread.
you can use just this function to solve this problem.
+ (NSString*) decodeHtmlUnicodeCharactersToString:(NSString*)str
{
NSMutableString* string = [[NSMutableString alloc] initWithString:str]; // #&39; replace with '
NSString* unicodeStr = nil;
NSString* replaceStr = nil;
int counter = -1;
for(int i = 0; i < [string length]; ++i)
{
unichar char1 = [string characterAtIndex:i];
for (int k = i + 1; k < [string length] - 1; ++k)
{
unichar char2 = [string characterAtIndex:k];
if (char1 == '&' && char2 == '#' )
{
++counter;
unicodeStr = [string substringWithRange:NSMakeRange(i + 2 , 2)];
// read integer value i.e, 39
replaceStr = [string substringWithRange:NSMakeRange (i, 5)]; // #&39;
[string replaceCharactersInRange: [string rangeOfString:replaceStr] withString:[NSString stringWithFormat:@"%c",[unicodeStr intValue]]];
break;
}
}
}
[string autorelease];
if (counter > 1)
return [self decodeHtmlUnicodeCharactersToString:string];
else
return string;
}