{"id":15637816,"url":"https://github.com/zacwest/zswtaggedstring","last_synced_at":"2025-04-15T01:07:07.429Z","repository":{"id":27700334,"uuid":"31187122","full_name":"zacwest/ZSWTaggedString","owner":"zacwest","description":"Converts Strings into NSAttributedStrings using an HTML-like markup language.","archived":false,"fork":false,"pushed_at":"2021-06-26T00:10:40.000Z","size":854,"stargazers_count":101,"open_issues_count":1,"forks_count":28,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-15T01:06:59.345Z","etag":null,"topics":["attributes","ios","objective-c","string","swift"],"latest_commit_sha":null,"homepage":"","language":"Objective-C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zacwest.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null},"funding":{"github":"zacwest"}},"created_at":"2015-02-23T00:00:43.000Z","updated_at":"2024-12-18T11:57:12.000Z","dependencies_parsed_at":"2022-08-21T05:50:20.643Z","dependency_job_id":null,"html_url":"https://github.com/zacwest/ZSWTaggedString","commit_stats":null,"previous_names":[],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacwest%2FZSWTaggedString","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacwest%2FZSWTaggedString/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacwest%2FZSWTaggedString/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zacwest%2FZSWTaggedString/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zacwest","download_url":"https://codeload.github.com/zacwest/ZSWTaggedString/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248986311,"owners_count":21194025,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["attributes","ios","objective-c","string","swift"],"created_at":"2024-10-03T11:12:57.304Z","updated_at":"2025-04-15T01:07:07.410Z","avatar_url":"https://github.com/zacwest.png","language":"Objective-C","funding_links":["https://github.com/sponsors/zacwest"],"categories":[],"sub_categories":[],"readme":"# ZSWTaggedString\n\n[![CI Status](https://img.shields.io/circleci/project/zacwest/ZSWTaggedString.svg?style=flat)](https://circleci.com/gh/zacwest/ZSWTaggedString)\n[![Version](https://img.shields.io/cocoapods/v/ZSWTaggedString.svg?style=flat)](http://cocoapods.org/pods/ZSWTaggedString)\n[![License](https://img.shields.io/cocoapods/l/ZSWTaggedString.svg?style=flat)](http://cocoapods.org/pods/ZSWTaggedString)\n[![Platform](https://img.shields.io/cocoapods/p/ZSWTaggedString.svg?style=flat)](http://cocoapods.org/pods/ZSWTaggedString)\n\nZSWTaggedString converts a `String`/`NSString` marked-up with tags into an  `NSAttributedString`. Tags are similar to HTML except you define what each tag represents.\n\nThe goal of this library is to separate presentation from string generation while making it easier to create attributed strings. This way you can decorate strings without concatenating or using hard-to-localize substring searches.\n\nThe most common example is applying a style change to part of a string. Let's format a string like \"bowties are **cool**\":\n\n```swift\nlet localizedString = NSLocalizedString(\"bowties are \u003cb\u003ecool\u003c/b\u003e\", comment: \"\");\nlet taggedString = ZSWTaggedString(string: localizedString)\n\nlet options = ZSWTaggedStringOptions()\n\noptions[\"b\"] = .static([\n    .font: UIFont.boldSystemFont(ofSize: 18.0)\n])\n\nlet attributedString = try! taggedString.attributedString(with: options)\nprint(attributedString)\n```\n\n```objective-c\nNSString *localizedString = NSLocalizedString(@\"bowties are \u003cb\u003ecool\u003c/b\u003e\", nil);\nZSWTaggedString *taggedString = [ZSWTaggedString stringWithString:localizedString];\n\nZSWTaggedStringOptions *options = [ZSWTaggedStringOptions options];\n[options setAttributes:@{\n    NSFontAttributeName: [UIFont boldSystemFontOfSize:18.0]\n          } forTagName:@\"b\"];\n\nNSLog(@\"%@\", [taggedString attributedStringWithOptions:options]);\n```\n\nThis produces an attributed string where the \"cool\" substring is bold, and the rest undefined:\n\n```objective-c\nbowties are {\n}cool{\n    NSFont = \"\u003cUICTFont: …\u003e …; font-weight: bold; …; font-size: 18.00pt\";\n}\n```\n\n## Dynamic attributes\n\nYou can apply style based on metadata included in the string. Let's italicize a substring while changing the color of a story based on its type:\n\n```swift\nlet story1 = Story(type: .One, name: \"on\u003ce\")\nlet story2 = Story(type: .Two, name: \"tw\u003co\")\n\nfunc storyWrap(_ story: Story) -\u003e String {\n    // You should separate data-level tags from the localized strings\n    // so you can iterate on their definition without the .strings changing\n    // Ideally you'd place this on the Story class itself.\n    return String(format: \"\u003cstory type='%d'\u003e%@\u003c/story\u003e\",\n        story.type.rawValue, ZSWEscapedStringForString(story.name))\n}\n\nlet format = NSLocalizedString(\"Pick: %@ \u003ci\u003eor\u003c/i\u003e %@\", comment: \"On the story, ...\");\nlet string = ZSWTaggedString(format: format, storyWrap(story1), storyWrap(story2))\n\nlet options = ZSWTaggedStringOptions()\n\n// Base attributes apply to the whole string, before any tag attributes.\noptions.baseAttributes = [\n    .font: UIFont.systemFont(ofSize: 14.0),\n    .foregroundColor: UIColor.gray\n]\n\n// Normal attributes just add their attributes to the attributed string.\noptions[\"i\"] = .static([\n    .font: UIFont.italicSystemFont(ofSize: 14.0)\n])\n\n// Dynamic attributes give you an opportunity to decide what to do for each tag\noptions[\"story\"] = .dynamic({ tagName, tagAttributes, existingAttributes in\n    var attributes = [NSAttributedString.Key: AnyObject]()\n    \n    guard let typeString = tagAttributes[\"type\"] as? String,\n        let type = Story.StoryType(rawValue: typeString) else {\n            return attributes\n    }\n    \n    switch type {\n    case .One:\n        attributes[.foregroundColor] = UIColor.red\n    case .Two:\n        attributes[.foregroundColor] = UIColor.orange\n    }\n    \n    return attributes\n})\n```\n\n```objective-c\nStory *story1 = …, *story2 = …;\n\nNSString *(^sWrap)(Story *) = ^(Story *story) {\n    // You should separate data-level tags from the localized strings\n    // so you can iterate on their definition without the .strings changing\n    // Ideally you'd place this on the Story class itself.\n    return [NSString stringWithFormat:@\"\u003cstory type='%@'\u003e%@\u003c/story\u003e\",\n            @(story.type), ZSWEscapedStringForString(story.name)];\n};\n\nNSString *fmt = NSLocalizedString(@\"Pick: %@ \u003ci\u003eor\u003c/i\u003e %@\", @\"On the story, ...\");\nZSWTaggedString *string = [ZSWTaggedString stringWithFormat:fmt,\n                           sWrap(story1), sWrap(story2)];\n\nZSWTaggedStringOptions *options = [ZSWTaggedStringOptions options];\n\n// Base attributes apply to the whole string, before any tag attributes.\n[options setBaseAttributes:@{\n    NSFontAttributeName: [UIFont systemFontOfSize:14.0],\n    NSForegroundColorAttributeName: [UIColor grayColor]\n }];\n\n// Normal attributes just add their attributes to the attributed string.\n[options setAttributes:@{\n    NSFontAttributeName: [UIFont italicSystemFontOfSize:14.0]\n          } forTagName:@\"i\"];\n\n// Dynamic attributes give you an opportunity to decide what to do for each tag\n[options setDynamicAttributes:^(NSString *tagName,\n                                NSDictionary *tagAttributes,\n                                NSDictionary *existingStringAttributes) {\n    switch ((StoryType)[tagAttributes[@\"type\"] integerValue]) {\n        case StoryTypeOne:\n            return @{ NSForegroundColorAttributeName: [UIColor redColor] };\n        case StoryTypeTwo:\n            return @{ NSForegroundColorAttributeName: [UIColor orangeColor] };\n    }\n    return @{ NSForegroundColorAttributeName: [UIColor blueColor] };\n} forTagName:@\"story\"];\n```\n\nYour localizer now sees a more reasonable localized string:\n\n```json\n\t/* On the story, ... */\n\t\"Pick: %@ \u003ci\u003eor\u003c/i\u003e %@\" = \"Pick: %1$@ \u003ci\u003eor\u003c/i\u003e %2$@\";\n```\n\nAnd you don't have to resort to using `.rangeOfString()` to format any of the substrings, which is very difficult to accomplish with what we desired above.\n\nThere are two types of dynamic attributes you can use: a tag-specific one like above, or the options-global `unknownTagAttributes` (`unknownTagDynamicAttributes` in Objective-C) which is invoked when an undefined tag is found. Both have three parameters:\n\n1. `tagName` The name of the tag, e.g. `story` above.\n2. `tagAttributes` The attributes of the tag, e.g. `[\"type\": \"1\"]` like above.\n3. `existingStringAttributes` The string attributes that exist already, e.g. `[NSForegroundColorAttributeName: UIColor.redColor()]`\n\nYou can use the `existingStringAttributes` to handle well-established keys. For example, let's make the `\u003cb\u003e`, `\u003ci\u003e`, and `\u003cu\u003e` tags automatically:\n\n```swift\nlet options = ZSWTaggedStringOptions()\n\noptions.baseAttributes = [\n    .font: UIFont.systemFont(ofSize: 12.0)\n]\n\noptions.unknownTagAttributes = .dynamic({ tagName, tagAttributes, existingAttributes in\n    var attributes = [NSAttributedString.Key: Any]()\n    \n    if let font = existingAttributes[.font] as? UIFont {\n        switch tagName {\n        case \"b\":\n            attributes[.font] = UIFont.boldSystemFont(ofSize: font.pointSize)\n        case \"i\":\n            attributes[.font] = UIFont.italicSystemFont(ofSize: font.pointSize)\n        default:\n            break\n        }\n    }\n    \n    if tagName == \"u\" {\n        attributes[.underlineStyle] = NSUnderlineStyle.single.rawValue\n    }\n    \n    return attributes\n})\n```\n\n```objective-c\nZSWTaggedStringOptions *options = [ZSWTaggedStringOptions options];\n[options setBaseAttributes:@{ NSFontAttributeName: [UIFont systemFontOfSize:12.0] }];\n\n[options setUnknownTagDynamicAttributes:^(NSString *tagName,\n                                          NSDictionary *tagAttributes,\n                                          NSDictionary *existingStringAttributes) {\n    BOOL isBold = [tagName isEqualToString:@\"b\"];\n    BOOL isItalic = [tagName isEqualToString:@\"i\"];\n    BOOL isUnderline = [tagName isEqualToString:@\"u\"];\n    UIFont *font = existingStringAttributes[NSFontAttributeName];\n\n    if ((isBold || isItalic) \u0026\u0026 font) {\n        if (isBold) {\n            return @{ NSFontAttributeName: [UIFont boldSystemFontOfSize:font.pointSize] };\n        } else if (isItalic) {\n            return @{ NSFontAttributeName: [UIFont italicSystemFontOfSize:font.pointSize] };\n        }\n    } else if (isUnderline) {\n        return @{ NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) };\n    }\n    \n    return @{};\n}];\n```\n\nThe library does not provide this functionality by default because custom or inexplicit fonts and dynamic type make this behavior unpredictable. You can use `ZSWTaggedStringOptions.registerDefaultOptions()` to keep a global default set of options with something like the above.\n\n## Fast stripped strings\n\nStripping tags allows you to create a `String` for fast height calculations (assuming no font changes), statistics gathering, etc., without the overhead of tags. You can accomplished this by using the `.string()` method on a `ZSWTaggedString` instead of the `.attributedString()` methods.\n\n## Error handling\n\nInvalid strings such as non-ending tags (`\u003ca\u003etaco!`) or strings where you do not escape user input (see [Gotchas](#gotchas)) are considered errors by the programmer.\n\nFor Swift consumers, all of the methods throw when you provide invalid input.\n\nFor Objective-C consumers, there are optional `NSError`-returning methods, and all of the methods return `nil` in the error case.\n\n## Gotchas\n\nIf any of your composed strings contain a `\u003c` character without being in a tag, you _must_ wrap the string with `ZSWEscapedStringForString()`. In practice, user-generated content where this is important is rare, but you must handle it.\n\n## Installation\n\nZSWTaggedString is available through [CocoaPods](http://cocoapods.org). Add the following line to your Podfile:\n\n```ruby\npod \"ZSWTaggedString\", \"~\u003e 4.2\"\npod \"ZSWTaggedString/Swift\", \"~\u003e 4.2\" # Optional, for Swift support\n```\n\n## License\n\nZSWTaggedString is available under the [MIT license](https://github.com/zacwest/ZSWTaggedString/blob/master/LICENSE). If you are contributing via pull request, please include an appropriate test for the bug you are fixing or feature you are adding.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzacwest%2Fzswtaggedstring","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzacwest%2Fzswtaggedstring","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzacwest%2Fzswtaggedstring/lists"}