blob: 239baa97f487881f6e3abcabdce3db6fed41c427 [file] [edit]
/*
Copyright 2018-present Google Inc. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
#import "NSString+MaterialBidi.h"
#import <CoreFoundation/CoreFoundation.h>
@implementation NSString (MaterialBidi)
// https://www.w3.org/International/questions/qa-bidi-unicode-controls
//TODO: Reach out to AAA about the utility of the Isolate markers
// ??? Do we want Embedding or Isolate markers? w3 recommends isolate?
// Add reference : UnicodeĀ® Standard Annex #9 UNICODE BIDIRECTIONAL ALGORITHM
// go/android-bidiformatter
// http://unicode.org/reports/tr9/
// Mark influences the directionality of neutral characters when the context is opposite of the
// neutral chatacter's desired directionality.
static NSString *kMDFLTRMark = @"\u200e"; // left-to-right mark
static NSString *kMDFRTLMark = @"\u200f"; // right-to-left mark
// Embedding indicates a text segment is embedded in a larger context with the opposite
// directionality.
static NSString *kMDFLTREmbedding = @"\u202a"; // left-to-right embedding
static NSString *kMDFRTLEmbedding = @"\u202b"; // right-to-left embedding
// Override reverses the directionality of strongly LTR or RTL characters
static NSString *kMDFLTROverride = @"\u202d"; // left-to-right override
static NSString *kMDFRTLOverride = @"\u202e"; // right-to-left override
// Pop is used to denote the end of an embedding or override text segment
static NSString *kMDFPopFormatting = @"\u202c"; // pop directional formatting
// Version 6.3.0 Bidi algorithm additions
// The following only work on iOS 10+
// Isolate indicates that the text segment has an internal directionality with no effect on
// surrounding characters.
static NSString *kMDFLTRIsolate = @"\u2066"; // left-to-right isolate
static NSString *kMDFRTLIsolate = @"\u2067"; // right-to-left isolate
static NSString *kMDFFirstStrongIsolate = @"\u2068"; // first strong isolate
// Pop Isolate is used to denote the end of an isolate text segment
static NSString *kMDFPopIsolate = @"\u2069"; // pop directional isolate
- (NSLocaleLanguageDirection)mdf_calculatedLanguageDirection {
// Attempt to determine language of string.
NSLocaleLanguageDirection languageDirection = NSLocaleLanguageDirectionUnknown;
// Pass string into CoreFoundation's language identifier
CFStringRef text = (__bridge CFStringRef)self;
CFRange range = CFRangeMake(0, (CFIndex)[self length]);
NSString *languageCode =
(NSString *)CFBridgingRelease(CFStringTokenizerCopyBestStringLanguage(text, range));
if (languageCode) {
// If we identified a language, explicitly set the string direction based on that
languageDirection = [NSLocale characterDirectionForLanguage:languageCode];
}
// If the result is not LTR or RTL, fallback to LTR
// ??? Should I be defaulting to NSLocale.NSLocaleLanguageCode.characterDiretion?
if (languageDirection != NSLocaleLanguageDirectionLeftToRight &&
languageDirection != NSLocaleLanguageDirectionRightToLeft) {
languageDirection = NSLocaleLanguageDirectionLeftToRight;
}
return languageDirection;
}
- (NSString *)mdf_stringWithBidiEmbedding {
NSLocaleLanguageDirection languageDirection = [self mdf_calculatedLanguageDirection];
return [self mdf_stringWithBidiEmbedding:languageDirection];
}
- (NSString *)mdf_stringWithBidiEmbedding:(NSLocaleLanguageDirection)languageDirection {
if (languageDirection == NSLocaleLanguageDirectionRightToLeft) {
return [NSString stringWithFormat:@"%@%@%@", kMDFRTLEmbedding, self, kMDFPopFormatting];
} else if (languageDirection == NSLocaleLanguageDirectionLeftToRight) {
return [NSString stringWithFormat:@"%@%@%@", kMDFLTREmbedding, self, kMDFPopFormatting];
} else {
// Return a copy original string if an unsupported direction is passed in.
return [self copy];
}
}
- (nonnull NSString *)mdf_stringWithStereoReset:(NSLocaleLanguageDirection)direction
context:(NSLocaleLanguageDirection)contextDirection {
#if DEBUG
// Disable in release, as a pre-caution in case not everyone defines NS_BLOCK_ASSERTION.
NSCAssert((contextDirection != NSLocaleLanguageDirectionLeftToRight ||
contextDirection != NSLocaleLanguageDirectionRightToLeft),
@"contextStringDirection must be passed in and set to either"
"NSLocaleLanguageDirectionLeftToRight or NSLocaleLanguageDirectionRightToLeft.");
NSCAssert((direction != NSLocaleLanguageDirectionLeftToRight ||
direction != NSLocaleLanguageDirectionRightToLeft ||
direction != NSLocaleLanguageDirectionUnknown),
@"stringToBeInsertedDirection must be set to either NSLocaleLanguageDirectionUnknown,"
"NSLocaleLanguageDirectionLeftToRight, or NSLocaleLanguageDirectionRightToLeft.");
#endif
if (self.length == 0) {
return [self copy];
}
if (direction != NSLocaleLanguageDirectionLeftToRight &&
direction != NSLocaleLanguageDirectionRightToLeft) {
direction = [self mdf_calculatedLanguageDirection];
}
NSString *bidiEmbeddedString = [self mdf_stringWithBidiEmbedding:direction];
NSString *bidiResetString;
if (direction != contextDirection) {
if (contextDirection == NSLocaleLanguageDirectionRightToLeft) {
bidiResetString =
[NSString stringWithFormat:@"%@%@%@", kMDFRTLMark, bidiEmbeddedString, kMDFRTLMark];
} else {
bidiResetString =
[NSString stringWithFormat:@"%@%@%@", kMDFLTRMark, bidiEmbeddedString, kMDFLTRMark];
}
} else {
bidiResetString = bidiEmbeddedString;
}
return bidiResetString;
}
- (NSString *)mdf_stringWithBidiMarkersStripped {
NSString *strippedString = self;
NSArray <NSString *>*directionalMarkers = @[ kMDFLTRMark,
kMDFRTLMark,
kMDFRTLEmbedding,
kMDFLTREmbedding,
kMDFRTLOverride,
kMDFLTROverride,
kMDFPopFormatting,
kMDFLTRIsolate,
kMDFRTLIsolate,
kMDFFirstStrongIsolate,
kMDFPopIsolate
];
for (NSString *markerString in directionalMarkers) {
strippedString =
[strippedString stringByReplacingOccurrencesOfString:markerString withString:@""];
}
return strippedString;
}
@end