I have a Chrome extension that replaces certain phrases on webpages. It uses a 2 dimensional array. The [i][1] replaces the text in provided in the [i][0] value.
for (var i = 0; i < array.length; i++) {
findAndReplaceDOMText(document.body, {
preset: 'prose',
find: array[i][0],
replace: array[i][1]
});
}
This code works fine yet it seems computationally expensive as it calls the findAndReplaceDOMText function multiple times rather than just once. Is it possible to move the for loop inside the function to just wrap the find and replace params? If so what would that look like? I can only get console errors trying this.
Edit: The function traverses the DOM looking for all visible human readable text that contains the regex phrase provided at find and replaces with the string provided at replace.
Without modifying the function's behaviour, you can't. What you could do is separating the find and replace passes.
Your strings are stored in a kind of difficult to transverse way, so let's flat them out:
var flatFind = array.map(function(elem){
return elem[0]
})
var flatReplace = array.map(function(elem){
return elem[1]
})
Then, you'd need to create a regex string that encompasses all your search strings:
var searchString = "/("+flatFind.join("|")+")/g"
Then pass it to the function, using a function to find the index of the match:
findAndReplaceDOMText(document.body, {
preset: 'prose',
find: searchString,
replace: function(portion, match){
var idx = flatFind.indexOf(match)
if(idx > -1) return flatReplace[idx]
else throw "hey, there's no replacement string for this!"
}
})
You could try to use the replace parameter as a function.
The find-configuration-property then needs to be a regular expression (RegExp-object) constructed of your search strings (like /first|second|third/g). Do not forget the g-modifier at the end.
As replace-configuration-property you then create a function that checks which string occurred (you get that as the second parameter to your function). According to the match you then return the corresponding value (for example if match is "first" then you return "1st". If match is "second" then you return "2nd" and so on).
Related
I'm trying to create a regex pattern to match "faux" html tags for a small application i am building.
I have created the regex to capture found matches within {tag}brackets{/tag} and output them into an array of objects like so:
{
{key : value},
{key : value}
}
Code with the current pattern:
let str = "{p}This is a paragraph{/p} {img}(path/to/image) {ul}{li}This is a list item{/li}{li}Another list item{/li}{/ul}";
let regex = /\{(\w+)}(?:\()?([^\{\)]+)(?:\{\/1})?/g;
let match;
let matches = [];
while (match = regex.exec(str)) {
matches.push({ [match[1]]: match[2]})
}
console.log(matches)
Link to JSbin
I have realized I need the pattern to capture nested groups as well, and put these into an array – so the result for the above string would be:
[
{p : "This is a paragraph"},
{img : "path/to/image"},
{ul : ["This is a list item", "Another List item"]}
]
The idea here is to match each tag in order, so that the indexes of the array match the order they are found (i.e. first paragraph in the string above is array[0] and so forth).
If anyone has a bit of input on how I could structure the pattern that would be greatly appreciated.
I will not have more than 1 level deep nesting, if that makes any difference.
I am flexible to use a different markup for the ul if this would help, however I cannot use square brackets [text] due to conflicts with another function that generates the text I am trying to extract in this step.
Edit: An idea that hit me is to have a third capturing group to capture and push to the list-array, but I am unsure whether or not this would work in reality? I have not gotten it to work so far
JavaScript has no support for recursion within regular expressions, which would otherwise be a potential solution.
I would however go for a different solution:
You could rely on DOMParser -- available in browsers, or if you are on Node, there is similar functionality available in several modules.
To use it, you need to have an XML formatted string, so unless you want to use <p> style of tags, you'd first have to convert your string to that, making sure that content with < would need to get < instead.
Also the {img} tag would need to get a closing tag instead of the parentheses. So a replacement is necessary for that particular case.
Once that is out of the way, it is quite straightforward to get a DOM from that XML, which might already be good enough for you to work with, but it can be simplified to your desired structure with a simple recursive function:
const str = "{p}This is a paragraph{/p} {img}(path/to/image) {ul}{li}This is a list item{/li}{li}Another list item{/li}{/ul}";
const xml = str.replace(/\{img\}\((.*?)\)/g, "{img}$1{/img}")
.replace(/</g, "<")
.replace(/\{/g, "<").replace(/\}/g, ">");
const parser = new DOMParser();
const dom = parser.parseFromString("<root>" + xml + "</root>", "application/xml").firstChild;
const parse = dom => dom.nodeType === 3 ? dom.nodeValue.trim() : {
[dom.nodeName]: dom.children.length
? Array.from(dom.childNodes, parse).filter(Boolean)
: dom.firstChild.nodeValue
};
const result = parse(dom).root;
console.log(result);
The output is almost what you intended, except that that li elements are also represented as { li: "...." } objects.
I'm obtaining a list of elements in classes and I'd like to filter out any class which does not contain a specific word in the innerHTML. Currently, I've managed to filter out the words however I can't seem to obtain the whole string, only part of it. Here's what I've got:
htmlData = $(htmlData).find(".Description").html("Fruit");
All this does is return "FruitFruitFruitFruitFruit..." when I'd like to obtain the whole string which the word is in. Anyone know how to do it? Thanks.
Wrap the assignment in an if, in which you're checking if the string is contained in the Text.
if($(htmlData).find(".Description").text().indexOf("Fruit") === -1){
htmlData = $(htmlData).find(".Description").html();
}
the indexOf function returns the index of where the string was found first. If it wasn't found, it returns -1.
I hope that this is what you meant..
var arr = [];
$.each($(htmlData).find(".Description"), function(_, jqElem){
var content = jqElem.html();
if (content.indexOf("Fruit") !== -1 ) {
arr.push(content);
}
})
If I have the following:
<p class="demo" id="first_p">
This is the first paragraph in the page and it says stuff in it.
</p>
I could use
document.getElementById("first_p").innerHTML
to get
This is the first paragraph in the page and it says stuff in it.
But is there something simple you can run which would return as a string
class="demo" id="first_p"
I know I can iterate through all of the element's attributes to get each one individually but is there a function which returns tagHTML or something like that?
The following code is something of a mouthful: I wrote it as a one-liner, but I've broken it out into several lines here. But this will get you a plain object where the keys are attribute names and the values are the values of the corresponding attributes:
Array.prototype.reduce.call(
document.getElementById('first_p').attributes,
function (attributes, currentAttribute) {
attributes[currentAttribute.name] = currentAttribute.value;
return attributes;
},
{}
);
Going through this, document.getElementById('first_p').attributes gets you a NamedNodeMap of the element's attributes. A NamedNodeMap is not an Array, but Array.prototype.reduce.call(...) calls Array.prototype.reduce on the NamedNodeMap as if it were an Array. We can do this because NamedNodeMap is written so that it can be accessed like an array.
But we can't stop here. That NamedNodeMap that I mentioned is an array of Attr objects, rather than an object of name-value pairs. We need to convert it, which is where the other arguments to Array.prototype.reduce come into play.
When it's not being called in a strange way, Array.prototype.reduce takes two arguments. The second argument (which is third for us because of the way we called it) is an object that we want to build up. In our case, that's a brand-new bare object: the {} that you see at the end.
The first argument to Array.prototype.reduce (which, again, is second for us) is another function. That function will get called once for each item in the loop, but it takes two arguments. The second argument is the current loop item, which is easy to understand, but the first argument is a little wild. The first time we call that function, its first argument is the object we want to build up (i.e. the last argument to Array.prototype.reduce. Each time after that, the first argument is whatever that function returned the last time it was called. Array.prototype.reduce returns whatever the last call to its inner function returned.
So we start with an empty object. Then for every Attr in the element's attributes, we add something to the object, and return it. When the last call finishes, the object is finished, so we return that. And this is how we make the attribute list.
If you wanted the exact code in the tag, like a String, then I'm afraid there is no standard function to get that exactly. But we can get a close approximation of that code, with a similar setup:
Array.prototype.map.call(
document.getElementById('first_p').attributes,
function (currentAttribute) {
return currentAttribute.name + '=' + JSON.stringify(currentAttribute.value);
}
).join(' ');
The basic principle is the same: we take that NamedNodeMap and call an Array function on it, but this time we're using map instead of reduce. You can think of map as a special case of reduce: it always builds up an Array, with one element for every element that was in the original. Because of that, you don't even need to mention the object you're building up: the callback function only has one argument, and we just return the thing to put into the new Array. Once we're done, we have an Array of 'name="value"' strings, and then we just join that with ' '.
It isn't a built-in property, but you can use the array-like object attributes to obtain what you're looking for.
Array.prototype.map.call(element.attributes, function (el) {
return el.name + '="' + el.value + '"';
}).join(' ')
This is assuming a browser that supports the map function. The Array.prototype.map.call part is because attributes is not really an array and does not have a join method, but because it's an array-like JavaScript's dynamism allows us to call map on it anyway.
Example from the current page with the footer div:
var element = document.getElementById('footer')
Array.prototype.map.call(element.attributes, function (el) {
return el.name + '="' + el.value + '"';
}).join(' ');
// "id="footer" class="categories""
You can try the following:-
var attributes = '';
for(var i=0; i<document.getElementById("first_p").attributes.length; i++){
var attr = document.getElementById("first_p").attributes[i];
attributes += attr.nodeName+"='"+attr.nodeValue+"' "
}
console.log(attributes);
You can use document.getElementById("first_p").attributes to get an array of all the attributes on that DOM element
If you wanted them all in one string just do: document.getElementById("first_p").attributes.join(' ') to get the desired output
Well, while nothing currently exists to do this directly (though the approaches using the Node's attributes is a more reliable approach, one option is to create this method yourself:
HTMLElement.prototype.tagHTML = function(){
// we create a clone to avoid doing anything to the original:
var clone = this.cloneNode(),
// creating a regex, using new RegExp, in order to create it
// dynamically, and inserting the node's tagName:
re = new RegExp('<' + this.tagName + '\\s+','i'),
// 'empty' variables for later:
closure, str;
// removing all the child-nodes of the clone (we only want the
// contents of the Node's opening HTML tag, so remove everything else):
while (clone.firstChild){
clone.removeChild(clone.firstChild);
}
// we get the outerHTML of the Node as a string,
// remove the opening '<' and the tagName and a following space,
// using the above regular expression:
str = clone.outerHTML.replace(re,'');
// naively determining whether the element is void
// (ends with '/>') or not (ends with '>'):
closure = str.indexOf('/>') > -1 ? '/>' : '>';
// we get the string of HTML from the beginning until the closing
// string we assumed just now, and then trim any leading/trailing
// white-space using trim(). And, of course, we return that string:
return str.substring(0,str.indexOf(closure)).trim();
};
console.log(document.getElementById('test').tagHTML());
console.log(document.getElementById('demo').tagHTML());
JS Fiddle demo.
I have the following code in my application. It is supposed to build a comma separated string from a JQuery collection. The collection is retrieved from some xml. I use JQuery each() to iterate. This is standard code that I use all the time. I declare and define the result variable (patientConditions) first and set it to blank. Within the function I add the found string to the result variable along with a comma. I am not bothered by the trailing comma this leaves if there are results. The problem is that with no results the second line within my each() is running - they probably both are. After the loop has completed (with no matching elements in the xml) the value of the result is ','. It should be blank. I think this is something to do with closures, or hoisting, but I am unable to figure out how its happening. I have hacked a solution to this scenario, but am more worried about the hole in my js knowledge :(
var patientConditions = '';
$xml.find('patient>prescription>conditions').each(function() {
var conditionName = $(this).find('condition>name');
patientConditions += conditionName.text() + ',';
});
From what I can understand there is a match for patient>prescription>conditions, but not for condition>name, in that case $(this).find('condition>name') will return a zero elemet set. then .text() on that set will return a empty string
$xml.find('patient>prescription>conditions').each(function() {
var conditionName = $(this).find('condition>name');
if(conditionName.length){
patientConditions += conditionName.text() + ',';
}
});
Whenever a jQuery object is used to find non existant nodes, in this case $(this).find('condition>name'). The jQuery object still exists, it just contains no association to a node. This will allow you to run all jQuery functions on this object despite it not having any reference. This is why conditionName.text() returns an empty string despite no node being present. The solution, check if the node exists before doing anything.
var patientConditions = '';
$xml.find('patient>prescription>conditions').each(function() {
var conditionName = $(this).find('condition>name');
if (conditionName.length > 0) {
patientConditions += conditionName.text() + ',';
} else {
// Do something if node doesnt exist
}
});
I need to rewrite a querysting using javascript.
First I check to see if the variable is present, if it is I want to replace it with a new search term. If it is not present then I just add the new variable
I'm able to get it to work with simple terms like hat, ufc hat
whitespaces are %20, so ufc hat is really ufc%20hat
I run into problem with terms like make-up, hat -baseball, coffee & tea, etc..
What is the proper regex for this?
Below is my code, which doesn't work.
var url = String(document.location).split('?');
querystring = url[1];
if(querystring.match(/gbn_keywords=/)!=null)
querystring=querystring.replace(/gbn_keywords=[a-zA-Z0-9%20.]+/,"gbn_keywords="+term);
else
querystring=querystring+"&gbn_keywords="+term;
No Regex needed. To get the query arguments, take everything after ?. Then, split the string by & to return each argument. Split again by = to get the arg name (right of =) and the value (left of =). Iterate through each argument, a rebuild the URL with each argument, excluding the one you don't want. You shouldn't run into problems here because ?, &, and - must be escaped if they are to be used in arguments. You also said you want to add the argument if it doesn't exist, so just set a variable to true, while you are iterating through each argument, if you find the argument. If you didn't append it to the end of the query string that you rebuilt.
location objects already have perfectly good properties like pathname, hostname etc. that give you the separate parts of a URL. Use the .search property instead of trying to hack the URL as a string (? may not only appear in that one place).
It's then a case of splitting on the & character (and maybe ; too if you want to be nice, as per HTML4 B2.2) and checking each parameter against the one you're looking for. For the general case this requires proper URL-decoding, as g%62n_keywords=... is a valid way of spelling the same parameter. On the way out naturally you will need to encode again, to stop & going on to the next parameter (as well as to include other invalid characters).
Here's a couple of utility functions you can use to cope with query string manipulation more easily. They convert between the ?... string as seen in location.search or link.search and a lookup Object mapping parameter names to arrays of values (since form-url-encoded queries can have multiple instances of the same parameter).
function queryToLookup(query) {
var lookup= {};
var params= query.slice(1).split(/[&;]/);
for (var i= 0; i<params.length; i++) {
var ix= params[i].indexOf('=');
if (ix!==-1) {
var name= decodeURIComponent(params[i].slice(0, ix));
var value= decodeURIComponent(params[i].slice(ix+1));
if (!(name in lookup))
lookup[name]= [];
lookup[name].push(value);
}
}
return lookup;
}
function lookupToQuery(lookup) {
var params= [];
for (var name in lookup)
for (var i= 0; i<lookup[name].length; i++)
params.push(encodeURIComponent(name)+'='+encodeURIComponent(lookup[name][i]));
return params.length===0? '' : '?'+params.join('&');
}
This makes the usage as simple as:
var lookup= queryToLookup(location.search);
lookup['gbn_keywords']= ['coffee & tea'];
var query= lookupToQuery(lookup);
& character is used to seperate key and value pairs in the querystring. So that you can match all the characters except for & by re-writing your code as follows:
querystring=querystring.replace(/gbn_keywords=[^&]+/,"gbn_keywords="+term);
[^&]+ matches one or more characters up to & or end of string. But if there may situations where the querystring data may look like ...?gbn_keywords= (no value) then a slight modification is needed to the above line:
querystring=querystring.replace(/gbn_keywords=[^&]*/,"gbn_keywords="+term);
Just change + to * so that the regex will match 0 or more characters. I think this is better.
Why don't you run a split on url[1] and than replace the value of the gbn_keywords in that new array?
And if you use a JavaScript Framework, there might be a handy function that does all that. In Prototype there is the function toQueryParams().