Why is my jQuery text.replace() function skipping some characters in strings - javascript

We have a carousel with some captions on each slide, and a function that checks for the letters LUMAX in each of the captions wrapped in an H4 tag and wraps the letter in a SPAN tag.
const arr = ['l','u','m','a','x'];
const re = new RegExp(`\\b${arr.join("|")}\\b`,"gi");
$("h4").html((_,text) => text.replace(re,match => `<span>${match}</span>`));
I thought this was working correctly, but on closer inspection it is not working for all characters. For instance - on the first slide all is good, but on the second slide the caption reads:
<h4>Love has no labels</h4>
For some reason it outputs like this
<h4><span>L</span>ove h<span>a</span>s no <span>l</span><span>a</span>bels</h4>
As you can see the first and second "L" are wrapped in SPAN tags, however the third instance of the letter L is not.
I tried rewriting the function in vanilla Javascript but this didn't work either.

Remove boundary \b from your regex and all is well.
Example:
const arr = ['l', 'u', 'm', 'a', 'x'];
const re = new RegExp(`${arr.join("|")}`, "gi");
$("h4").html((_, text) => text.replace(re, match => `<span>${match}</span>`));
span {
color: red
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<h4>Love has no labels</h4>

Related

How to set the tittle attribute value based on the value in a map?

I want to add for every words existing as a key in a given map a span tag. I did like that:
const map = new Map(Object.entries(JSON.parse(json)));
const words = [...map.keys()];
var data = document.getElementById("num1").value
$('#fake_textarea').html(data);
// create a regular expression matching any of these words, using 'g' flag to match all instances
var regexp = new RegExp('(' + words.join('|') + ')', 'ig');
$('#fake_textarea').each(function (num, elem) {
var text = $(elem).text();
// use string.replace with $& notation to indicate whatever was matched
text = text.replace(regexp, `<span title="$&" id="$&" style="color:red;"><b>$&</b></span>`);
$(elem).html(text);
});
It works well. how I can add for every added span the title attribute with the correspondig value in the map. let say for a given word key I need to add the attribute <span title=value of key in the map> key. For example:
and my map is:
So I need to replace the tooltip of boy that displays boy by Mr.
Indeed, how to match words consisting of two words?
You can use String.prototype.replace() with a function as a replacer:
const html = $('#fake_textarea').val().replace(
/\p{L}+/gu, // Match every word respecting Unicode letters
word => {
const wordLowercased = word.toLowerCase();
if (!map.has(wordLowercased)) {
return word;
}
return $('<span>')
.text(word)
.attr('title', map.get(wordLowercased))
.css({
color: 'red',
fontWeight: 'bold',
})
.prop('outerHTML');
}
);
Make sure all your words in the map are lowercased to match them.
Notice that I match every word instead of producing one long regex pattern. Doing the latter can lead to wrong results if there are special characters in the map or, what's even worse, could crash your app if the regex turns out to be wrong (using some special characters in the pattern, like { without closing }).

Wrap all individual words in a span tag based on their first letter

I am trying to wrap each individual word on a webpage in a tag so I can style them individually based on their starting letter.
I have found this method of wrapping each word in a span tag individually, but I can't figure out how to vary the class based on the first letter of the word.
let e = document.getElementById('words');
e.innerHTML = e.innerHTML.replace(/(^|<\/?[^>]+>|\s+)([^\s<]+)/g, '$1<span class="word">$2</span>');
What you're trying to achieve can't be done with regex if you want to reference the individual words.
I've wrote a little snippet that uses document.querySelector() instead
outerText property on the query selector object returns a plain text string which is later converted to an array with split() function
Then it simply loops over the array and appends the style tag and to get the first letter I've used substring() function
const words = document.querySelector("#words").outerText.split(" ");
const wordsDiv = document.querySelector("#words")
wordsDiv.innerHTML = ""
words.map((el) => {
wordsDiv.innerHTML += `<span class="${el.substring(0, 1)}">${el}</span> `
})
<div id="words">red green blue orange</div>
Try this:
let e = document.getElementById('words');
// Create array with words
let words = e.innerHTML.split(' ');
// Object with CSS classes and corresponding letter
const classes = {
a: 'class-one',
b: 'class-two',
// and so on
}
// Return array with the new strings
words = words.map(word => {
const firstLetter = word.substring(0,1);
return `<span class="${classes[firstLetter]}">${word}</span>`;
});
// Join the array and update the DOM
e.innerHTML = words.join(' ');
Here the following is happening:
Words are being separated by a space, creating an array;
The classes constant must receive, as in the example, the correspondence of each letter with its class;
In the function of the map method, the first letter of the word is being returned and the classes object is accessed with that letter;
Finally we do a join to join all the texts separating them with a space.
Remembering that if there is more than one space between words, inconsistencies will occur, as the first letter will be a space.
From the string.prototype.replace reference, you can also add a replace function to the method, instead of an string. The replace function has this form.
So, if I didn't misinterpret your problem, you can do something similar to this:
let e = document.getElementById('words');
e.innerHTML = e.innerHTML.replace(/(^|<\/?[^>]+>|\s+)([^\s<]+)/g, function(match, p1, p2) {
const myClasses = {
a: "aword",
b: "bword",
...
}
return `${p1}<span class="${myClasses[p2[0]]}">${p2}</span>'
});
You could do something like this:
Select all the children
map them with their text content
Split the text into single words
map again with some styled html
Join, render and enjoy.
words.innerHTML = [...words.children].flatMap(el => el.innerText.replace(/\n/ig, " ").split(" ")).map(el => `<div class="someClass">${el}</div>`).join("")
.someClass {
color: red;
background: orange;
margin: 10px;
}
<div id="words">
<div>
Some dummy content
<span>Some other nested dummy content</span>
</div>
<p>Some sibling content</p>
</div>

Regex to get parts of a string

all i am struggling to get sections of a string with regex without using Split or any other similar function here is my scenario:
I have this text U:BCCNT.3;GOwhich i want to get the different sections divided but the symbols in the middle I have managed to get the first one with this regex /(.+):/.exec(value) this gives me the first word till the colon(:) and these are the different variations of the value
Second section BCCNT
BCCNT.3;GO -> without the U: so the string might also contain no colon so for the second section the logic would be any text that is between : and . or any text ending with . and nothing infront
Third section .3-> any text starting with a . and ending with nothing or anytext staring with a . and ending with a ; semicolon
Fourth section ;GO-> any text starting with a ; and ending with nothing
EDIT
and preferably on separate variables like
const sectionOne = regex.exec(value);
const sectionTwo = regex.exec(value);
const sectionThree = regex.exec(value);
const sectionFour = regex.exec(value);
and which ever value doesnt match the pattern the variable would just be undefined or null or any empty string
Here is a regex approach using 4 separate optional capture groups for each possible component:
var input = "U:BCCNT.3;GO";
var re = /^([^:]+:)?([^.]+)?(\.[^;]+)?(;.*)?$/g;
var m;
m = re.exec(input);
if (m) {
console.log(m[1], m[2], m[3], m[4]);
}
Something like
/^(?:([^:]*):)?([^.]*)\.(?:([^;]*);(.*))?/
For example:
const s = 'U:BCCNT.3;GO';
const m = s.match(/^(?:([^:]*):)?([^.]*)\.(?:([^;]*);(.*))?/);
console.log(m);

replace and regex exception

I want to wrap all the words of a text in a <trans> tag, to be able to work on each words. Hover them, translate on click etc.
For that I need an exception in my replace function to ignore html tags like <br> or <span>.
Here is the function I have :
function wrapWords(str, tmpl) {
return str.replace(/(?![<br>\<span class="gras">\</span>])[a-zA-ZÀ-ÿ]+/gi, tmpl || "<trans>$&</trans>");
}
This function is working well with russian characters but not with french ones. The problem is that the <br> and <span> exception is excluding french characters b,r,s,p,a... Because of that some words are not wrapped correctly in my <trans> tag.
Does anyone knows how could I exclude a group of characters like specific tags <br> for example without affecting letters b and r in french ?
Thanks for any answer!
Properly using DOM, it is a bit more complex, but no corner cases to worry about, as it is very straightforward.
You want to split the text, thus it makes sense to only operate on text nodes. To find all text nodes, we could evaluate an XPath, or we could construct a TreeWalker.
Once we know which nodes we want to operate on, we take one node at a time and get all-space and no-space sequences. Each will be transformed into another text node, but the no-space sequences will additionally be wrapped inside a <span>. We append them one by one in front of the original node, which will guarantee the correct order, then finally we'll remove the original node, when the replacement nodes are all in their place.
function getTextNodes(node) {
let walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false);
let textnodes = [];
let textnode;
while (textnode = walker.nextNode()) {
textnodes.push(textnode);
}
return textnodes;
}
function wrap(element) {
getTextNodes(element).forEach(node => {
node.textContent.replace(/(\S+)|(\s+)/g, (match, word, space) => {
let textnode = document.createTextNode(match);
let newnode;
if (word) {
newnode = document.createElement('trans');
newnode.appendChild(textnode);
} else {
newnode = textnode;
}
node.parentNode.insertBefore(newnode, node);
});
node.remove();
});
}
wrap(document.getElementById('wrapthis'));
trans {
background-color: pink;
}
Not affected<br/>
<div id="wrapthis">
This is affected<br>
<span class="gras">HTML tags are fine</span><br/>
This as well<br/>
</div>
Not affected<br/>
Here's a quick way:
"foo bar baz".split(" ").map(w => "<trans>" + w + "</trans>").join(" ");
Explanation:
sentence is splitted by space character, which gives an Array. Each element of this Array is then wrapped in <trans> tags. Then everything is joined to create back a string.
Edit: usage in the DOM:
var sourceTextNode = document.createElement("div"); // here you're supposed to get an existing node...
sourceTextNode.textContent = "foo bar baz"; // ... and doing this is for the example purposes
sourceTextNode.innerHTML = sourceTextNode.textContent.split(" ").map(w => "<trans>" + w + "</trans>").join(" ");
sourceTextNode is:
<div>
<trans>foo</trans>
<trans>bar</trans>
<trans>baz</trans>
</div>
Note: You may want to exclude empty elements in the splitted Array that you'll get when there are multiple consecutive space charcaters.
One way to do this is testing the non-emptiness of the elements in a filter:
sourceText.split(" ").filter(Boolean)...

Regular expression for getting first and last name ignoring middle names

I'm searching a regular expression that could give me the first and last name in a string that is a complete name.
I searched but I didn't find one that fit my needs. For instance:
Abc Def Ghi Jkl ---> Abc Jkl
Aéc Def Gài Mkl ---> Aéc Mkl
Aéc-Def Gài Mkl ---> Aéc-Def Mkl
Aéc Def Gài-Mkl ---> Aéc Gài-Mkl
Afd ---> Afd
How can I build a regex to return me what is on the right side when the string is what is on the left?
To your specific case where you have different chars, you must change the regex a little to suit your need, here is one that achieves what you need:
^([\w-éà]+)[^\w-éà].*?[^\w-éà]([\w-éà]+)$|^([\w-éà]+)$
Tested on regex101.com:
Explanation:
We must break the regex into two parts to make it easier to understand:
^([\w-éà]+)[^\w-éà].*?[^\w-éà]([\w-éà]+)$
This is the general case where you have at least two names.
The block [\w-éà] represents your character set.
You then use the start anchor (^) to tell the engine that you are looking for a match at the start of the line, then you get a group containing your characters set, until you find something that is not in your character set([^\w-éà]). you then use lazy quantifiers .*? to match the first occurrence of the next pattern which is to match a word to the end anchor($).
The second part is just the one word case (^([\w-éà]+)$)
In this example group 1 will have first name when there is at least two names
group 2 will have last name when there is at least two names
and group 3 will have the name when there is only one name
While I wouldn't suggest regular expressions for this, the following, using String.prototype.split(), Array.prototype.shift() and Array.prototype.forEach(), seems much easier:
function firstAndLast(el) {
// getting the text of the element:
var haystack = el.textContent,
// splitting that text on white-space sequences,
// forming an array:
names = haystack.split(/\s+/),
// getting the first element of that array:
first = names.shift(),
// initialising the 'last' variable to an empty string:
last = '';
// if the names array has a length greater than 1
// (there is more than one name):
if (names.length > 1) {
// last is assigned the last element of the array of names:
last = names.pop();
}
// return an array containing the first and last names:
return [first, last];
}
// getting all the <li> elements in the document:
var listItems = document.querySelectorAll('li'),
// creating an empty <span> element:
span = document.createElement('span'),
// an unitialised variable for use within the loop:
clone;
// iterating over each of the <li> elements, using
// Array.prototype.forEach(), and Function.prototype.call():
Array.prototype.forEach.call(listItems, function(li) {
// cloning the created <span>:
clone = span.cloneNode();
// setting the clone's text to the joined-together
// strings from the Array returned by the function:
clone.textContent = firstAndLast(li).join(' ');
// appending that cloned created-<span> to the
// current <li> element over which we're iterating:
li.appendChild(clone);
});
function firstAndLast(el) {
var haystack = el.textContent,
names = haystack.split(/\s+/),
first = names.shift(),
last = '';
if (names.length > 1) {
last = names.pop();
}
return [first, last];
}
var listItems = document.querySelectorAll('li'),
span = document.createElement('span'),
clone;
Array.prototype.forEach.call(listItems, function(li) {
clone = span.cloneNode();
clone.textContent = firstAndLast(li).join(' ');
li.appendChild(clone);
});
li span::before {
content: ' found: ';
color: #999;
}
li span {
color: #f90;
width: 5em;
}
<ol>
<li>Abc Def Ghi Jkl</li>
<li>Aéc Def Gài Mkl</li>
<li>Aéc-Def Gài Mkl</li>
<li>Aéc Def Gài-Mkl</li>
<li>Afd</li>
</ol>
JS Fiddle demo.
It is possible to use regular expressions, just needlessly more complex:
function firstAndLast(el) {
var haystack = el.textContent,
// matching a case-insensitive sequence of characters at the
// start of the string (^), that are in the range a-z,
// unicode accented characters, an apostrophe or
// a hyphen (escaped with a back-slash because the '-'
// character has a special meaning within regular
// expressions, indicating a range, as above) followed
// by a word-boundary (\b):
first = haystack.match(/^[a-z\u00C0-\u017F'\-]+\b/i),
// as above but the word-boundary precedes the string of
// of characters, and it matches a sequence at the end
// of the string ($):
last = haystack.match(/\b[a-z\u00C0-\u017F'\-]+$/i);
// if first exists (no matching regular expression would
// would return null) and it has a length:
if (first && first.length) {
// we assign the first element of the array returned by
// String.prototype.match() to the 'first' variable:
first = first[0];
}
if (last && last.length) {
// as above:
last = last[0];
}
// if the first and last variables are exactly equal,
// we return only the first; otherwise we return both
// first and last, in both cases within an array:
return first === last ? [first] : [first, last];
}
function firstAndLast(el) {
var haystack = el.textContent,
first = haystack.match(/^[a-z\u00C0-\u017F'\-]+\b/i),
last = haystack.match(/\b[a-z\u00C0-\u017F'\-]+$/i);
if (first && first.length) {
first = first[0];
}
if (last && last.length) {
last = last[0];
}
return first === last ? [first] : [first, last];
}
var listItems = document.querySelectorAll('li'),
span = document.createElement('span'),
clone;
Array.prototype.forEach.call(listItems, function(li) {
clone = span.cloneNode();
clone.textContent = firstAndLast(li).join(' ');
li.appendChild(clone);
});
li span::before {
content: ' found: ';
color: #999;
}
li span {
color: #f90;
width: 5em;
}
<ol>
<li>Abc Def Ghi Jkl</li>
<li>Aéc Def Gài Mkl</li>
<li>Aéc-Def Gài Mkl</li>
<li>Aéc Def Gài-Mkl</li>
<li>Afd</li>
</ol>
JS Fiddle demo.
References:
CSS:
CSS pseudo-elements, ::before and ::after.
JavaScript:
Array.prototype.forEach().
Array.prototype.join().
Array.prototype.push().
Array.prototype.shift().
document.createElement().
document.querySelectorAll().
Element.cloneNode().
Function.prototype.call().
Guide to JavaScript Regular Expressions.
Node.textContent.
String.prototype.match().
String.prototype.split().
I would use ^ to match the beginning of the input, then parentheses () , the special \w character, and the + character to capture the first name. Then optional whitespace/characters, followed by more parentheses to capture the last name right before the end of the input, which is matched by the special $ character. Here's an example:
var huge = 'Abc Def Ghi Jkl';
var small = 'Afd';
var regex = /^(\w+).*?(\w*)$/;
var results = regex.exec(huge);
console.log(results[1]); // 'Abc'
console.log(results[2]); // 'Jkl'
var results = regex.exec(small);
console.log(results[1]); // 'Afd'
There are many ways to do what you want, so I recommend reading this page.
if you're passing only one full name for the regular expression use this one to get the first and last name
/^[^ \n]+|[^ \n]+$/g , and if you're passing the list of all full names separated by a line between each full name use this /^[^ \n]+|[^ \n]+$/gm just by adding m at the end of the regex use this link for testing regex to get first and last name from a full name
Remember a well structured Regex should cover as many exceptions as possible not only the current existing examples - plus it should be designed in a way to be easily extended in future! In JS you can try the following Regex:
var re = /^(\w+(-\w+)? ?)((.* )(?!$))?(\w+(-\w+)?)$/;
var strLong = "Abc_Def-John with a Really really_LongName";
var newstrLong = strLong.replace(re, "$1$5");
console.log(newstrLong);
var strShort = "simplyJohn";
var newstrShort = strShort.replace(re, "$1$5");
console.log(newstrShort);

Categories

Resources