Swift - Add attributed string to items in parenthesis - regex

I'm trying to display text to a view from a set of definitions and in cases where that text/string has parenthesis, I would like to display that parenthesis element in a different way - let's say bolded vs non bolded.
"This string a"
"This string b (has parenthesis)" - parenthesis show in lower
weight font
Now I'm aware that the solution is found by combining regular expressions - \(\w*\) - with attributed strings, but I haven't been able to combine it meaningfully.
This is my function that prints the words
func setWord(_ index:Int) {
if (index < 0) { return }
let word:[String:AnyObject] = self.definitions[index]
if let wordLabelText = word[self.store.sourceLanguage.lowercased()] as? String {
self.wordLabel.attributedText = NSMutableAttributedString(string: wordLabelText, attributes: [NSKernAttributeName: 1.0])
}
print(word)
self.definitionView.word = word
}
What I've done so far is added some more definitions for the attributed strings output, but then I'm not sure how to continue:
func setWord(_ index:Int) {
if (index < 0) { return }
let font = UIFont.preferredFont(forTextStyle: UIFontTextStyle.headline)
let fontSize = font.pointSize * 3
let plainFont = UIFont(name: "X-BoldItalic", size: fontSize)
let boldFont = UIFont(name: "X-Italic", size: fontSize)
let word:[String:AnyObject] = self.definitions[index]
if let wordLabelText = word[self.store.sourceLanguage.lowercased()] as? String {
self.wordLabel.attributedText = NSMutableAttributedString(string: wordLabelText, attributes: [NSKernAttributeName: 1.0, NSFontAttributeName: boldFont])
Here I'd need to loop through the words looking for the (elements), but I'm not sure how to do that and properly return the words. Am I on the right path? Thanks a bunch :)

A solution in Objective-C, with explained logic that should be easily translated in Swift.
NSDictionary *defaultAttributes = #{NSFontAttributeName: [UIFont boldSystemFontOfSize:15]};
NSDictionary *otherAttributes = #{NSFontAttributeName: [UIFont systemFontOfSize:12]};
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:#"\\(.*?\\)" options:0 error:nil];
NSString *initialString = #"This string a (has parenthesis), This string b (has parenthesis too), This string C hasn't.";
NSMutableAttributedString *attr = [[NSMutableAttributedString alloc] initWithString:initialString attributes:defaultAttributes];
NSLog(#"Attr: %#", attr);
NSArray *allMatches = [regex matchesInString:[attr string] options:0 range:NSMakeRange(0, [attr length])];
for (NSTextCheckingResult *aResult in allMatches)
{
[attr addAttributes:otherAttributes range:[aResult range]];
}
NSLog(#"Attr: %#", attr);
Logs:
$> Attr: This string a (has parenthesis), This string b (has parenthesis too), This string C hasn't.{
NSFont = "<UICTFont: 0x14663df0> font-family: \".SFUIText-Semibold\"; font-weight: bold; font-style: normal; font-size: 15.00pt";
}
$> Attr: This string a {
NSFont = "<UICTFont: 0x14663df0> font-family: \".SFUIText-Semibold\"; font-weight: bold; font-style: normal; font-size: 15.00pt";
}(has parenthesis){
NSFont = "<UICTFont: 0x14666260> font-family: \".SFUIText\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}, This string b {
NSFont = "<UICTFont: 0x14663df0> font-family: \".SFUIText-Semibold\"; font-weight: bold; font-style: normal; font-size: 15.00pt";
}(has parenthesis too){
NSFont = "<UICTFont: 0x14666260> font-family: \".SFUIText\"; font-weight: normal; font-style: normal; font-size: 12.00pt";
}, This string C hasn't.{
NSFont = "<UICTFont: 0x14663df0> font-family: \".SFUIText-Semibold\"; font-weight: bold; font-style: normal; font-size: 15.00pt";
}
What is the idea:
Create a NSMutableAttributedString with the "default" attributes (in our case bold font with "big size").
Then create a NSRegularExpression and find all occurrences.
You'll add the attributes (in our case small and normal font) at that occurrence range.
In our case, it works simply, because since you can only have one attribute per kind maximum at a specific range, the NSFontAttributeName attribute will be replaced for that range.
If you added more attributes, and want to remove them, you may need to not call addAttributes:range:, but replaceCharactersInRange:withAttributedString: instead:
NSAttributedString *replacement = [[NSAttributedString alloc] initWithString:[[attr string] substringWithRange:[aResult range]]
attributes:otherAttributes];
[attr replaceCharactersInRange:[aResult range] withAttributedString:replacement];
Edit: Swift 3 Version
Nota Bene: I'm clearly not a Swift Developer, this code seems to work, but clearly, I "write" Swift like I write Objective-C, and many things since I don't use them daily and didn't read the doc are wrongly done (like the conversion/cast/explicit type/class, the "as", the "!", the "?", etc.), but it could be a start for you.
If you are a Swift developer and spots issues, feel free to comment the post and suggest your modifications. If you're just here because you have the same issue, don't forget to read in the comment if there are more Swifty things to fix in my pseudo code.
let defaultAttributes:[String:Any] = [NSFontAttributeName:UIFont.boldSystemFont(ofSize:15)];
let otherAttributes:[String:Any] = [NSFontAttributeName:UIFont.systemFont(ofSize:12)];
do {
let regex = try NSRegularExpression.init(pattern: "\\(.*?\\)", options: [])
let initialString:String = "This string a (has parenthesis), This string b (has parenthesis too), This string C hasn't."
let attr = NSMutableAttributedString.init(string: initialString, attributes: defaultAttributes)
print("Attr\(attr)");
let allMatches:[NSTextCheckingResult] = regex.matches(in: attr.string, options:[], range: NSRange(location: 0, length: attr.string.characters.count))
for aResult in allMatches
{
let occurrence = (attr.string as NSString).substring(with: aResult.range)
let replacement = NSAttributedString.init(string: occurrence , attributes: otherAttributes)
attr.replaceCharacters(in: aResult.range, with: replacement)
//attr.addAttributes(otherAttributes, range: aResult.range)
}
print("Attr\(attr)");
} catch let regexError {
print(regexError)
}

Related

Remove empty space in modified text

I have a function that takes in text and checks for the # symbol. The output is the same text, but any words following the # symbol will be coloured, similar to that of a social media mention. The problem is that it adds an extra empty space to the front of the original text. How do I modify the output to remove the empty space it adds to the front of the new text?
func textWithHashtags(_ text: String, color: Color) -> Text {
let words = text.split(separator: " ")
var output: Text = Text("")
for word in words {
if word.hasPrefix("#") { // Pick out hash in words
output = output + Text(" ") + Text(String(word))
.foregroundColor(color) // Add custom styling here
} else {
output = output + Text(" ") + Text(String(word))
}
}
return output
}
Just call the function in a view like
textWithHashtags("Hello #stackoverflow how is it going?", color: .red)
try something like this:
func textWithHashtags(_ text: String, color: Color) -> Text {
let words = text.split(separator: " ")
var output: Text = Text("")
var firstWord = true // <-- here
for word in words {
let spacer = Text(firstWord ? "" : " ") // <-- here
if word.hasPrefix("#") { // Pick out hash in words
output = output + spacer + Text(String(word))
.foregroundColor(color) // Add custom styling here
} else {
output = output + spacer + Text(String(word))
}
firstWord = false
}
return output
}

How to change color of text following a specific symbol in text field and text editor? SwiftUI

I have this function which when called in a view will, take the inputted String, and output the same string, but with words following the # symbol changed in colour:
func textWithSymbols(_ text: String, color: Color) -> Text {
let words = text.split(separator: " ")
var output: Text = Text("")
for word in words {
if word.hasPrefix("#") { // Pick out # in words
output = output + Text(" ") + Text(String(word))
.foregroundColor(color) // Add custom styling here
} else {
output = output + Text(" ") + Text(String(word))
}
}
return output
}
I would call it like:
var txt = "What do you think #test"
textWithSymbols(txt, color: .red)
The result would be the same text, but #test would be highlighted in red.
How would I apply this same functionality to both a text field and text editor where text is constantly being updated?
I've used the .onChange modifier before for stuff like counting lines and replacing text, so maybe that might be a start?

How to color table rows with 3 alternate color

I want a table to display with 3 alternate colors (1-black,2-red,3-white,4-black, 5-red,6-white ....) i tried with nth-child(even) and nth-child(odd)
but how to get alternate 3 row colors
Following to the w3c, you try this:
tr:nth-child(3n+1) {
background-color: black;
}
tr:nth-child(3n+2) {
background-color: red;
}
tr:nth-child(3n) {
background-color: white;
}

Search body for a string matched by regexp and replace

I want search a div for a string like "12345" and then put every matched string into a span.
But when find repetitive string, just do it for first matched several time.
Here is a jsfiddle:
function find(){
var regex = new RegExp(/12345/g),
list = $(".test").html().match(regex);
console.log(list)
for(each in list){
replacement = $(".test").html().replace(list[each], "<span class='box'>"+list[each]+"</span>");
$(".test").html(replacement);
}
}
find();
.box{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="test">
<p>
12345 12345
</p>
</div>
Your approach is faulty: rather than extracting all matching substrings and later iterate them performing single replacements, you may use your own regex inside a String#replace method to modify the substrings "inline", "on-the-match" way:
function find(){
var regex = /12345/g;
var replacement = $(".test").html().replace(regex, "<span class='box'>$&</span>");
$(".test").html(replacement);
}
find();
.box{
color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="test">
<p>
12345 12345
</p>
</div>
My solution (fiddle here), with pure JavaScript :
function find(){
var motif = "12345"
var regex = new RegExp(motif, "g")
document.querySelector("div.test").innerHTML = document.querySelector("div.test").innerHTML.replace(regex, "<span class='box'>" + motif + "</span>")
}
find()

Truncating paragraph text with a fixed hight and no bleed

See attached image. (http://i.imgur.com/SWlUllK.jpg)
I have three adjacent paragraphs which I would like to truncate before the "Read More" call-to-action.
The catch is that each of the articles has a fixed height so that my truncation buttons ("Read More") can line up on the same horizontal as you can see in the image.
The problem I'm facing here is that the length of the excerpt is dependant on the length of the heading text. If there is a three line heading then the length of the excerpt will be shorter due to the increased height that the header is taking up.
At the moment I'm using some jQuery to do character count and truncate after X words. It's occurred to me that that is not a viable solution.
I couldn't figure out a way to use CCS either because the button at the bottom is absolutely positioned so overflow:hidden ignores the height of the button and just flows behind it cutting it off at the end of the article.
The only thing that I've thought of is forcing the article to be a specific height and then moving the "Read More" button outside the article tag but that doesn't seem like good semantics to me.
Any thoughts on this?
Sass:
article {
border-right: 1px solid #e7e7e7;
height: 506px;
position: relative;
p {
font-size: emCalc(16px);
overflow: hidden;
margin-bottom: 0.6em;
max-height: 255px;
}
p + a {
position: absolute;
bottom: 0;
display: block;
background: $lightBlue;
padding: 0.8em;
color: #fff;
text-align: center;
}
}
I good way of doing it is having "overflow-y: scroll" on your article HTML element and then removing text until the ".scrollHeight == .offsetHeight" of the HTML element.
http://jsfiddle.net/qdxTj/
Here's the straight JavaScript.
var all = document.getElementsByTagName("div");
for(var i=0; i<all.length; i++) {
var article = all[i];
if(article.className && /(^|\s)article($|\s)/.test(article.className)) {
article.scrollTop = 1;
var cnt = 0;
while(article.scrollTop != 0 || article.scrollHeight != article.offsetHeight) {
cnt++; if(cnt > 50) break;
var ps = article.getElementsByTagName("p");
if(ps.length == 0)
break;
var p = ps[ps.length - 1];
var shorter = p.innerHTML;
var idx = shorter.lastIndexOf(" ");
shorter = idx >= 0 ? shorter.substring(0, idx) : "";
p.innerHTML = shorter;
if(p.innerHTML.length == 0)
article.removeChild(p);
article.scrollTop = 1;
}
article.style.overflowY = "hidden";
}
}