I have a little jQuery script that works like Emmet/ZenCoding and that expands keywords to a snippet on the press of the TAB key in a textarea.
It works fine when there's only one occurence of the keyword, but I don't know how to replace only the last occurence of a word in a string.
Here's an example string that would be typed in the textarea :
Have a look at the rules : rules
In this case, the user would press the TAB key after the second rules, and the script fetches the corresponding value in an array, to replace the word with an actual link to the rules. It should (in theory) be replaced by this :
Have a look at the rules : http://website.com/rules.php
My problem is that I'm using replace and it will replace all occurences of that word, which is not the desired behavior. Here is the relevant part of the script :
var content = $(this).val();
content = content.replace(lastWord, insertSnippets[lastWord]);
$(this).val(content);
It first gets the content in the textarea at the time, replace the word (rules) with the corresponding value (the actual link). Is there any way to replace only the last occurence? It won't always be the word "rules", there are a dozen of other keywords of unknown length.
TL;DR How do I replace only the last occurence of a word with jQuery/Javascript?
I was thinking of getting the current position of the caret and replace only the word to its left, but getting the position of the caret seems rather tedious - and I still wouldn't know how to do the replace.
This might do it, so long as your search string doesn't include any regex special characters:
var replaceLast = function(searchFor, replaceWith, str) {
return str.replace(
new RegExp("^(.*)" + searchFor + "(.*?)$"),
function(_, before, after) {return before + replaceWith + after;}
);
};
replaceLast("rules", "http://website.com/rules.php",
"Have a look at the rules : rules", "rules");
//=> "Have a look at the rules : http://website.com/rules.php"
If this function were passed to some curry function, then you could create a function such as updateRules via
var updateRules = replaceLast("rules", "http://website.com/rules.php");
// ... later
updateRules("Have a look at the rules : rules", "rules");
//=> "Have a look at the rules : http://website.com/rules.php"
But the original function might be all you need.
Here's what I came up with (only tested webkit) using a contenteditable field.
It does a few things nicely, though probably inefficiently:
Replaces word with a related snippet that live in an object
replaces only the current word your caret is on
Sets your cursor to the end of the line after tabbing to complete
Try typing in "blah blah blah rules" and hit tab after "rules". Try it with multiple "rules" in the string.
var content = document.getElementById('content')
content.addEventListener('keydown', function (event) {
var _this = this,
text = this.innerHTML;
if ( event.keyCode == 9 ) {
event.preventDefault()
var sel = window.getSelection(),
currentWord = getWord(),
val = sel.anchorNode.nodeValue,
snippet = tabComplete(currentWord),
range = sel.getRangeAt(0)
var selArray = sel.anchorNode.nodeValue.split(' ')
if ( snippet !== undefined ) {
var currentWordPosition = [ (range.endOffset - currentWord.length), range.endOffset ]
var newText = text.splice(currentWordPosition[0], currentWordPosition[1], ' '+snippet)
_this.innerHTML = newText;
sel.collapse(_this.firstChild, sel.anchorNode.nodeValue.length)
}
}
})
var snippets = {
'rules' : 'http://website.com/rules.php',
'faq' : 'http://website.com/faq.php'
}
function tabComplete(string) {
if ( typeof string !== 'string' ) { return false; }
var snippetKeys = Object.keys(snippets)
for ( var i=0; i<snippetKeys.length; i++ ) {
if ( snippets[string] ) {
return snippets[string]
}
}
}
function getWord() {
var range = window.getSelection().getRangeAt(0);
if ( range.collapsed ) {
text = range.startContainer.textContent.substring(0, range.startOffset+1);
return text.split(/\b/g).pop();
}
return '';
}
String.prototype.splice = function(start, length, replacement) {
return this.substr(0,start)+replacement+this.substr(start+length);
}
#content {
width:350px;
height:150px;
background-color: #333;
color: white;
font-family: sans-serif;
padding:10px;
}
<div id="content" contenteditable></div>
Use a regex and only match the end of the content string.
/yourWord$/
will match yourWord if it is at the very end of the string
Just tweak the regex a bit:
var content = $(this).val();
content = content.replace(lastWord+'.*?$', insertSnippets[lastWord]);
$(this).val(content);
Related
I'm trying to create a userscript (Tampermonkey) to add some helper buttons into a site and originally I was using the script below based on the one posted here.
setInterval(function() {
//RegEx for finding any string of digits after "ID: " up to the next space
var myRegexp = /ID:\s(\d*?)\s/gi;
];
var txtWalker = document.createTreeWalker (
document.body,
NodeFilter.SHOW_TEXT,
{ acceptNode: function (node) {
//-- Skip whitespace-only nodes
if (node.nodeValue.trim() )
return NodeFilter.FILTER_ACCEPT;
return NodeFilter.FILTER_SKIP;
}
},
false
);
var txtNode = null;
while (txtNode = txtWalker.nextNode () ) {
var oldTxt = txtNode.nodeValue;
//Find all instances
match = myRegexp.exec(oldTxt);
while (match != null) {
//Get group from match
var idNum = match[1]
//Replace current match with added info
oldTxt = oldTxt.(idNum, idNum+"| <SomeHTMLHere> "+idNum+" | ");
//Update text
txtNode.nodeValue = oldTxt;
//Check for remaining matches
match = myRegexp.exec(oldTxt);
}
}
}, 5000);
Now I would like to add a bit more functionality to the text, probably something clickable to copy to clipboard or insert elsewhere. Now I know I'm working with text nodes in the original script but I wanted to know if there was anyway of adapting the current script to insert HTML at these points without rewriting from scratch.
The main problem with the site is these ID:##### values I'm search for all appear within the same element like below so I could simply find them by element (or at least not with my limited JS knowledge).
<div>
ID: 1234567 | Text
ID: 45678 | Text
</div>
If someone could point me in the right direction that'd be great or at least tell me it isn't possible without a rewrite.
Okay, so rewriting it actually worked out pretty well. Works much more nicely and is more succinct. Hopefully this will help someone in the future. If anyone wants to suggest any improved, please feel free!
setInterval(function() {
//Regex
var reg = /ID:\s(\d*?)\s/gi;
var result;
//Get all classes that this applies to
$('.<parentClass>').each(function(i, obj) {
var text = $(this).html();
//Do until regex can't be found anymore
while (result = reg.exec(text)) {
//Get first regex group
var str = result[1];
//Add in desired HTML
var newhtml = $(this).html().replace(str, '|<span class="marked">' + str + '</span>|');
//Replace
$(this).html(newhtml);
}
});
//Click function for added HTML
$(".marked").click(function(){
//Get text inside added HTML
var id = $(this).text();
//Construct desired string
var Str = "someText " + id;
//Insert into message box
textarea = document.querySelector('.<inputArea>')
textarea.value = Str;
textarea.focus();
});
}, 5000);
I need to locate words for more than 4 characters that are written between <p> </p> in uppercase and add them a style (ex. italic).
I know about the function isUpperCase() but don't know how to apply it to check if the string is more than 4 characters.
function isUpperCase( string ) {
(?)
}
Thanks.
var ps = [].slice.call(document.getElementsByTagName("p"))
ps.forEach(function (p) {
p.textContent.split(" ").forEach(function (word) {
if (word.length > 4 && word.toUpperCase() === word) {
// 4character UPPERCASE word
}
})
})
You could use a regex to replace any uppercase text longer than four characters in the innerHTML of every <p> element with that text surrounded by the markup you're trying to insert:
$('p').each(function(){
var pattern = /([-A-Z0-9]{4,})/g;
var before = '<span style="color: red;">';
var after = '</span>';
$(this).html($(this).html().replace(pattern, before+"$1"+after));
});
http://jsfiddle.net/eHPVg/
Yeah, like Rob said, I don't think Raynos's answer will work cross-browser and it also won't let you modify the matches within the paragraph.
Here's a slightly modified version:
var i = 0, ps = document.getElementsByTagName("p");
for(len = ps.length; i<len; i++)
{
var p = ps[i];
p.innerHTML = p.innerHTML.replace(/\b([A-Z]{4,})\b/g, "<span style='font-style:italic'>$1</span>";
}
You can change the span code to be whatever style you want to add. Just make sure to leave the $1, which refers the original uppercase word.
I am trying to find given word in HTML string and add a span around it.
What I am doing now is this:
function find(what:String,where:String)
{
var regexp:RegExp=new RegExp(what,'gi');
return where.replace(regexp,'<span>$&</span>');
}
It works well on words that are not inside HTML tags.
What I want is to ignore those that are inside HTML tags.
Example: find("spain")
Input:
The rain in <b class="spain">Spain</b> stays mainly in the <i data-test="Spain">plain</i>.
Output:
The rain in <b class="spain"><span>Spain</span></b> stays mainly in the <i data-test="Spain">plain</i>.
How can I achieve this, please?
To account for html tags and attributes that could match, you are going to need to parse that HTML one way or another. The easiest way is to add it to the DOM (or just to a new element):
var container = document.createElement("div");
container.style.display = "none";
document.body.appendChild(container); // this step is optional
container.innerHTML = where;
Once parsed, you can now iterate the nodes using DOM methods and find just the text nodes and search on those. Use a recursive function to walk the nodes:
function wrapWord(el, word)
{
var expr = new RegExp(word, "i");
var nodes = [].slice.call(el.childNodes, 0);
for (var i = 0; i < nodes.length; i++)
{
var node = nodes[i];
if (node.nodeType == 3) // textNode
{
var matches = node.nodeValue.match(expr);
if (matches)
{
var parts = node.nodeValue.split(expr);
for (var n = 0; n < parts.length; n++)
{
if (n)
{
var span = el.insertBefore(document.createElement("span"), node);
span.appendChild(document.createTextNode(matches[n - 1]));
}
if (parts[n])
{
el.insertBefore(document.createTextNode(parts[n]), node);
}
}
el.removeChild(node);
}
}
else
{
wrapWord(node, word);
}
}
}
Here's a working demo: http://jsfiddle.net/gilly3/J8JJm/3
You won't be able to process HTML in any reliable way using regex. Instead, parse the HTML into a DOM tree and iterate the Text nodes checking their data for content.
If you are using JavaScript in a web browser, the parsing will have already have been done for you. See this question for example wrap-word-in-span code. It's much trickier if you need to match phrases that might be split across different elements.
function find(what:String,where:String)
{
what = what.replace(/(\[|\\|\^|\$|\.|\||\?|\*|\+|\(|\)|\{|\})/g, "\\$1")
.replace(/[^a-zA-Z0-9\s:;'"~[\]\{\}\-_+=(),.<>*\/!##$%^&|\\?]/g, "(?:&[0-9A-Za-z]{3,25};|&#[0-9]{1,10};?|[^\s<])")
.replace(/</g,"<?").replace(/>/g,">?").replace(/"/g,"(?:\"|"?)")
.replace(/\s/g, "(?:\\s| ?)");
what = "(>[^<]*|^[^<]*)(" + what + ")";
var regexp:RegExp=new RegExp(what,'gi');
return where.replace(regexp,'$1<span>$2</span>');
}
The first replace function adds a backslash before characters which have a special meaning in a RE, to prevent errors or unexpected results.
The second replace function replaces every occurrence of unknown characters in the search query by (?:&[0-9A-Za-z]{3,25};|&#[0-9]{1,10};?|[^\s<]). This RE consists of three parts: First, it tries to match a HTML entity. Second, it attempts to match a HTML numeric entity. Finally, it matches any non-whitespace character (in case the creator of the HTML document didn't properly encode the characters).
The third, fourth and fifth replace functions replaces <, > and " by the corresponding HTML entities, so that the search query will not search through tags.
The sixth replace function replaces white-space by a RE (\s| ?), which match white-space characters and the HTML entity.
The only shortcoming of this function is that undocumented special characters (such as €) match any HTML entity/character (following the example, not only € and € are valid matches, but also £ and #).
This proposed solution suits in most cases. It can be inaccurate in complex situations, which is probably not worse than a DOM iteration (which is very susceptible to memory leaks and requires more computing power).
When you work with HTML elements which have Event listeners assigned through DOM, you should iterate through all (child) elements, and apply this function to every Text node.
Pure JavaScript (based on Sizzle.getText from jQuery); Demo: http://jsfiddle.net/vol7ron/U8LLv/
var wrapText = function ( elems,regex ) {
var re = new RegExp(regex);
var elem;
for ( var i = 0; elems[i]; i++ ) {
elem = elems[i];
// Get the text from text nodes and CDATA nodes
if ( elem.nodeType === 3 || elem.nodeType === 4 ) {
parent = elem.parentNode;
re.lastIndex = 0;
if(re.test(elem.nodeValue)){
var span = document.createElement('span');
span.innerHTML = RegExp.$1;
if (RegExp.leftContext != ''){
parent.insertBefore(document.createTextNode(RegExp.leftContext),elem); i++;
}
parent.insertBefore(span,elem); i++;
if (RegExp.rightContext != ''){
parent.insertBefore(document.createTextNode(RegExp.rightContext),elem); i++;
}
parent.removeChild(elem);
}
// Traverse everything else, except comment nodes
} else if ( elem.nodeType !== 8 ) {
wrapText( elem.childNodes, regex );
}
}
return;
};
var obj = document.getElementById('wrapper');
wrapText([obj],/(spain)/gi);
I am allowing my users to wrap words with "*", "/", "_", and "-" as a shorthand way to indicate they'd like to bold, italicize, underline, or strikethrough their text. Unfortunately, when the page is filled with text using this markup, I'm seeing a noticeable (borderline acceptable) slow down.
Here's the JavaScript I wrote to handle this task. Can you please provide feedback on how I could speed things up?
function handleContentFormatting(content) {
content = handleLineBreaks(content);
var bold_object = {'regex': /\*(.|\n)+?\*/i, 'open': '<b>', 'close': '</b>'};
var italic_object = {'regex': /\/(?!\D>|>)(.|\n)+?\//i, 'open': '<i>', 'close': '</i>'};
var underline_object = {'regex': /\_(.|\n)+?\_/i, 'open': '<u>', 'close': '</u>'};
var strikethrough_object = {'regex': /\-(.|\n)+?\-/i, 'open': '<del>', 'close': '</del>'};
var format_objects = [bold_object, italic_object, underline_object, strikethrough_object];
for( obj in format_objects ) {
content = handleTextFormatIndicators(content, format_objects[obj]);
}
return content;
}
//#param obj --- an object with 3 properties:
// 1.) the regex to search with
// 2.) the opening HTML tag that will replace the opening format indicator
// 3.) the closing HTML tag that will replace the closing format indicator
function handleTextFormatIndicators(content, obj) {
while(content.search(obj.regex) > -1) {
var matches = content.match(obj.regex);
if( matches && matches.length > 0) {
var new_segment = obj.open + matches[0].slice(1,matches[0].length-1) + obj.close;
content = content.replace(matches[0],new_segment);
}
}
return content;
}
Change your regex with the flags /ig and remove the while loop.
Change your for(obj in format_objects) loop with a normal for loop, because format_objects is an array.
Update
Okay, I took the time to write an even faster and simplified solution, based on your code:
function handleContentFormatting(content) {
content = handleLineBreaks(content);
var bold_object = {'regex': /\*([^*]+)\*/ig, 'replace': '<b>$1</b>'},
italic_object = {'regex': /\/(?!\D>|>)([^\/]+)\//ig, 'replace': '<i>$1</i>'},
underline_object = {'regex': /\_([^_]+)\_/ig, 'replace': '<u>$1</u>'},
strikethrough_object = {'regex': /\-([^-]+)\-/ig, 'replace': '<del>$1</del>'};
var format_objects = [bold_object, italic_object, underline_object, strikethrough_object],
i = 0, foObjSize = format_objects.length;
for( i; i < foObjSize; i++ ) {
content = handleTextFormatIndicators(content, format_objects[i]);
}
return content;
}
//#param obj --- an object with 2 properties:
// 1.) the regex to search with
// 2.) the replace string
function handleTextFormatIndicators(content, obj) {
return content.replace(obj.regex, obj.replace);
}
Here is a demo.
This will work with nested and/or not nested formatting boundaries. You can omit the function handleTextFormatIndicators altogether if you want to, and do the replacements inline inside handleContentFormatting.
Your code is forcing the browser to do a whole lot of repeated, wasted work. The approach you should be taking is this:
Concoct a regex that combines all of your "target" regexes with another that matches a leading string of characters that are not your special meta-characters.
Change the loop so that it does the following:
Grab the next match from the source string. That match, due to the way you changed your regex, will be a string of non-meta characters followed by your matched portion.
Append the non-meta characters and the replacement for the target portion onto a separate array of strings.
At the end of that process, the separate accumulator array can be joined and used to replace the content.
As to how to combine the regular expressions, well, it's not very pretty in JavaScript but it looks like this. First, you need a regex for a string of zero or more "uninteresting" characters. That should be the first capturing group in the regex. Next should be the alternates for the target strings you're looking for. Thus the general form is:
var tokenizer = /(uninteresting pattern)?(?:(target 1)|(target 2)|(target 3)| ... )?/;
When you match that against the source string, you'll get back a result array that will contain the following:
result[0] - entire chunk of string (not used)
result[1] - run of uninteresting characters
result[2] - either an instance of target type 1, or null
result[3] - either an instance of target type 2, or null
...
Thus you'll know which kind of replacement target you saw by checking which of the target regexes are non empty. (Note that in your case the targets can conceivably overlap; if you intend for that to work, then you'll have to approach this as a full-blown parsing problem I suspect.)
You can do things like:
function formatText(text){
return text.replace(
/\*([^*]*)\*|\/([^\/]*)\/|_([^_]*)_|-([^-]*)-/gi,
function(m, tb, ti, tu, ts){
if(typeof(tb) != 'undefined')
return '<b>' + formatText(tb) + '</b>';
if(typeof(ti) != 'undefined')
return '<i>' + formatText(ti) + '</i>';
if(typeof(tu) != 'undefined')
return '<u>' + formatText(tu) + '</u>';
if(typeof(ts) != 'undefined')
return '<del>' + formatText(ts) + '</del>';
return 'ERR('+m+')';
}
);
}
This will work fine on nested tags, but will not with overlapping tags, which are invalid anyway.
Example at http://jsfiddle.net/m5Rju/
I have a string (partly HTML) where I want to replace the string :-) into bbcode :wink:. But this replacement should not happen within <pre>, but in any other tag (or even not within a tag).
For example, I want to replace
:-)<pre>:-)</pre><blockquote>:-)</blockquote>
to:
:wink:<pre>:-)</pre><blockquote>:wink:</blockquote>
I already tried it with the following RegEx, but it does not work (nothing gets replaced):
var s = ':-)<pre>:-)</pre><blockquote>:-)</blockquote>';
var regex = /:\-\)(?!(^<pre>).*<\/pre>)/g;
var r = s.replace(regex, ':wink:');
Can someone please help me? :-)
This ought to do it:-
var src = ":-)<pre>:-)</pre><blockquote>:-)</blockquote>"
var result = src.replace(/(<pre>(?:[^<](?!\/pre))*<\/pre>)|(\:\-\))/gi, fnCallback)
function fnCallback(s)
{
if (s == ":-)") return ":wink:"
return s;
}
alert(result);
It works because any pre element will get picked up by the first option in the regex and once consumed means that any contained :-) can't be matched since the processor will have moved beyond it.
You could avoid hellish regexes altogether if you use a suitable library such as jQuery, e.g.:
var excludeThese = ['pre'];
// loop over all elements on page, replacing :-) with :wink: for anything
// that is *not* a tag name in the excludeThese array
$('* not:(' + excludeThese.join(',') + ')').each(function() {
$(this).html($(this).html().replace(/:\-\)/,':wink:'));
});
Just thought it'd be worth offering a DOM solution:
E.g.
var div = document.createElement('div');
div.innerHTML = ":-)<pre>:-)</pre><blockquote>:-)</blockquote>";
replace(div, /:-\)/g, ":wink:", function(){
// Custom filter function.
// Returns false for <pre> elements.
return this.nodeName.toLowerCase() !== 'pre';
});
div.innerHTML; // <== here's your new string!
And here's the replace function:
function replace(element, regex, replacement, filter) {
var cur = element.firstChild;
if (cur) do {
if ( !filter || filter.call(cur) ) {
if ( cur.nodeType == 1 ) {
replace( cur, regex, replacement );
} else {
cur.data = cur.data.replace( regex, replacement );
}
}
} while ( cur = cur.nextSibling );
}
Almost good: Your negative lookbehind and lookahead where not in the right position and need a slight adjustment:
/(?<!(<pre>)):-\)(?!(<\/pre>))/g
Looks for all ":-)"
...but not if there is a <pre> behind (the regex cursor is!)
...but not if there is a </pre> before (the regex cursor is!)
as a side effect though: <pre>:-):-)</pre> works too but not <pre>:-):-):-)</pre>
https://regex101.com/r/CO0DAD/1
ps. this is a firefox 104 browser (could be different in others)
try with
var regex = /:-)(?!(^)*</pre>)/g;