% Placeholder iterative format one by one

I have an NSAttributedString that looks like

"Somestring bold% @ pattern% f% d blah blah blah"

I want to be able to replace parts of a format template, as in [NSString stringWithFormat: ...], but preserving the style so that replaced strings correspond to the style around them (everything is shown in bold in my example above).

Is there a way to iterate each placeholder of the% format one by one so that I can use the argument list to populate the string?

I do not want to create my own implementation of%, because I know that it is a million and one other format.

Or is there an easier solution that I skip?

Edit: I will explain a bit of the complete solution that I solve:

So that my team can attribute localized strings, I already have a way to write

"key"="test //italic %@// **bold** __underline %d__"; 

If the specifier is between the attributed tags, I want this part to be assigned. Currently, I can create an attributed string as shown above, the next part is to take care of the qualifiers left above.

I do this in the order of the Parse attributes -> Apply arguments ..., I could easily solve it in another way, but I don't want the arguments in the format to randomly bind to the attributes

+6
source share
3 answers

Thanks so much @Rick for pointing me in the right direction.

His post recommends going over to arguments first and avoiding any strings or characters before applying format strings. This led me to another problem that I encountered earlier when trying to repeat the list of arguments of different types (NSString, int, etc.), as NSLog does. I think I discovered that this it is impossible (or at least very difficult) to do this, and the reason why the NSLog may be that it knows what types to expect through format specifiers (% @,% i, etc.).

I understand that in fact I can get the same effect not by escaping the arguments, but by escaping the format string itself.

Example:

 format: "test //italic %@//" args: "text // more text" 

Steps:

  • First replace all // instances with // - TAG - //
  • Apply arguments
  • Determine where the style is applied between // - TAG - //

Obviously, // - TAG - // can still be written in arguments to ruin the styling, however, depending on what you use as a replacement, the chances of this occur are essentially null.

+2
source

I do this in the order of the attributes Parse -> Apply arguments ..., I can easily solve it in another way, but I don't need format arguments to spoil the attributes

Why not just add an escape character? As far as I understand, you risk the example that you provided the opportunity to get messed up if the first line contains a double slash?

 "key"="test //italic %@// **bold** __underline %d__"; 

if %@ is text // more text , which would ruin the formatting.

If so, just parse each vararg of type NSString and char to make sure they don't contain any characters reserved for your attributes. If so, add an escape char before which you delete when parsing attributes.

The above example would look like this:

 "key"="test //italic text \/\/ more text// **bold** __underline 34__"; 

After that, you analyze the attributes the same way as before, but ignore the characters preceding \ , and do not forget to delete \ .

This is a bit more complicated, but I'm sure it is much less than implementing your own printf-style analyzer.

+1
source

Here is the working code:

 #import <Foundation/Foundation.h> @interface NSAttributedString (AttributedFormat) - (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ...; - (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments; @end @implementation NSAttributedString (AttributedFormat) - (instancetype)initWithFormat:(NSAttributedString *)attrFormat, ... { va_list args; va_start(args, attrFormat); self = [self initWithFormat:attrFormat arguments:args]; va_end(args); return self; } - (instancetype)initWithFormat:(NSAttributedString *)attrFormat arguments:(va_list)arguments { NSRegularExpression *regex; regex = [[NSRegularExpression alloc] initWithPattern: @"(%.*?[@%dDuUxXoOfeEgGccsSpaAF])" options: 0 error: nil]; NSString *format = attrFormat.string; format = [regex stringByReplacingMatchesInString: format options: 0 range: NSMakeRange(0, format.length) withTemplate: @"\0$1\0"]; NSString *result = [[NSString alloc] initWithFormat:format arguments:arguments]; NSMutableArray *f_comps = [format componentsSeparatedByString:@"\0"].mutableCopy; NSMutableArray *r_comps = [result componentsSeparatedByString:@"\0"].mutableCopy; NSMutableAttributedString *output = [[NSMutableAttributedString alloc] init]; __block int consumed_length = 0; [attrFormat enumerateAttributesInRange:NSMakeRange(0, attrFormat.length) options:0 usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop) { NSMutableString *substr = [NSMutableString string]; while(f_comps.count > 0 && NSMaxRange(range) >= consumed_length + [(NSString *)f_comps[0] length]){ NSString *f_str = f_comps[0]; NSString *r_str = r_comps[0]; [substr appendString:r_str]; [f_comps removeObjectAtIndex:0]; [r_comps removeObjectAtIndex:0]; consumed_length += f_str.length; } NSUInteger idx = NSMaxRange(range) - consumed_length; if(f_comps.count > 0 && idx > 0) { NSString *f_str = f_comps[0]; NSString *leading = [f_str substringToIndex:idx]; [substr appendString:leading]; NSString *trailing = [f_str substringFromIndex:idx]; [f_comps replaceObjectAtIndex:0 withObject:trailing]; [r_comps replaceObjectAtIndex:0 withObject:trailing]; consumed_length += idx; } [output appendAttributedString:[[NSAttributedString alloc] initWithString:substr attributes:attrs]]; }]; return [self initWithAttributedString:output]; } @end 

Usage example:

 NSMutableAttributedString *fmt = [[NSMutableAttributedString alloc] initWithString:@"test: "]; [fmt appendAttributedString: [[NSAttributedString alloc] initWithString: @"Some%%string" attributes: @{ NSFontAttributeName: [UIFont systemFontOfSize:17] }]]; [fmt appendAttributedString: [[NSAttributedString alloc] initWithString: @"bold %@ template %.3f %d" attributes: @{ NSFontAttributeName: [UIFont boldSystemFontOfSize:20], NSForegroundColorAttributeName: [UIColor redColor] }]]; [fmt appendAttributedString: [[NSAttributedString alloc] initWithString: @"%@ blah blah blah" attributes: @{ NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor blueColor] }]]; NSAttributedString *result = [[NSAttributedString alloc] initWithFormat:fmt, @"[foo]", 1.23, 56, @"[[bar]]"]; 

Result:

screenshot

It may still have some errors, but it should work in most cases.


 (%.*?[@%dDuUxXoOfeEgGccsSpaAF]) 

This regular expression matches "Format Specifiers . " the qualifier begins with % and ends with the specified characters. and may have some modifiers in between. This is not ideal, for example, this illegal format "%__s" should be ignored, but my regular expression matches this entire line. but as long as the specifier is legal, it should work.

My code matches it and inserts delimiters around qualifiers:

 I'm %s. I'm <delimiter>%s<delimiter>. 

I use \0 as a delimiter.

 I'm \0%s\0. 

then interpolate it.

 I'm \0rintaro\0. 

then split the format and result into a separator:

 f_comps: ["I'm ", "%s", "."] r_comps: ["I'm ", "rintaro", "."] 

Here, the total length of the f_comps string f_comps exactly the same as the original attribute format. Then, iterating over the attributes using enumerateAttributesInRange , we can apply the attributes to the results.

Sorry, but it’s too difficult to explain the work inside enumerateAttributesInRange , with my weak English skills :)

+1
source

Source: https://habr.com/ru/post/978660/


All Articles