I'm trying to write a simple function that iterates through text and replaces any href's it comes across with text instead;
var REPLACE = [
{expression: www.anyhref.com, value: 'website'}
];
function(instance) {
instance = function {
var insteadURL;
insteadURL: = REPLACE.match(function(expression) {
return element.classList.contains(expression);
});
return(
insteadURL ? insteadURL.value : getElementText(expression)
);
}
}
I feel as though I may not be using the match method or the conditional operator properly but from what I understand this should work. But of course it doesn't.
If you're trying to replace links (i guess) in your text, try this regex:
/<a.*?href="www.anyhref.com".*?\/a>/g
Then you would have to add for each href you want to replace an entry in your array.
If you are in DOM context you can do the following:
To iterate through the DOM you can use this function:
function domReplace(node, iterator) {
switch (node && node.nodeType) {
case 1: case 9: case 11: {
const newNode = iterator((node.nodeName || '').toLowerCase(), node);
if (newNode && newNode != node && node.parentNode) {
node.parentNode.insertBefore(newNode, node);
node.parentNode.removeChild(node);
}
for (let child = newNode.firstChild; child; child = child.nextSibling)
domReplace(child, iterator);
} break ;
case 3: {
const newNode = iterator('#text', node);
if (newNode && newNode != node && node.parentNode) {
node.parentNode.insertBefore(newNode, node);
node.parentNode.removeChild(node);
}
} break ;
}
}
Then you can replace a if pattern matches .href with custom text:
domReplace(document, (type, node) => {
if (type == 'a') {
for (let i = 0; i < REPLACE.length; i += 1)
if (~(node.href || '').indexOf(REPLACE[i].expression))
return document.createTextNode(REPLACE[i].value);
return document.createTextNode(node.href);
}
return node;
});
Note you should not give document to domReplace but the right dom node to avoid full page replacement
Related
I have a function that preprocesses a Text Node in the HTML DOM.
The purpose is essentially to perform some interpolation or string templating.
The function basically checks for occurrences that match the regular expressions /\${([^}]*)}/g and /{{([^}]*)}/g.
Examples: ${foo + 1} and {{foo + 2}}
That is all working.
But my goal is to replace these occurrences with new nodes (e.g. span) consisting of Knockout binding expressions containing the inner values of the matches from the regular expression. With positions being correct. Preserving whitespaces where they occur.
Like so:
<span ko-text="foo" /> for ${foo} (Note: Custom binding syntax)
I just cant wrap my head around it with TextNode.splitText.
How do I achieve this?
This is the code I've got so far:
preprocessNode(node: Element) {
if ("nodeValue" in node && node.nodeValue !== null) {
var value = node.nodeValue;
var match = value.matchAll(/\${([^}]*)}/g);
if (!match) {
match = value.matchAll(/{{([^}]*)}/g);
}
if (match !== null && match.length > 0) {
var parentNode = node.parentNode;
for (let entry of match) {
var offset = node.nodeValue.indexOf(entry[0]);
var oldNode = node;
node = node.splitText(offset);
var newNode = document.createElement("span");
newNode.setAttribute("ko-text", entry[1]);
node.parentNode.appendChild(newNode);
}
return [parentNode, parentNode];
}
}
return null;
}
The function matchAll is a custom function.
Disclaimer: I don't know JavaScript! This is just a suggestion.
# /^\$\{([^{}]+)\}|\{\{([^{}]+)\}\}|((?:(?!^\$\{[^{}]+\}|\{\{[^{}]+\}\})[\S\s])+)/
^ \$\{
( [^{}]+ ) # (1)
\}
|
\{\{
( [^{}]+ ) # (2)
\}\}
|
( # (3 start)
(?:
(?!
^ \$\{ [^{}]+ \}
| \{\{ [^{}]+ \}\}
)
[\S\s]
)+
) # (3 end)
Pseudo-code:
if ( regex_find ( /^\$\{[^{}]+\}|\{\{[^{}]+\}\}/, value ) )
{
var found = false;
while ( regex_search ( /^\$\{([^{}]+)\}|\{\{([^{}]+)\}\}|((?:(?!^\$\{[^{}]+\}|\{\{[^{}]+\}\})[\S\s])+)/g, match, value ) )
{
if ( match[1] != null )
{
// Found '${..}' form
found = true;
var newNode = document.createElement("span");
newNode.setAttribute("ko-text", match[1]);
// Append newNode
}
else
if ( match[2] != null )
{
// Found '{{..}}' form
found = true;
var newNode = document.createElement("span");
newNode.setAttribute("ko-text", match[2]);
// Append newNode
}
else
if ( match[3] != null )
{
// Found '...' normal text
found = true;
var newNode = document.createTextNode( match[3] );
// Append newNode
}
}
if ( found )
{
// Clear or delete the original text node
// ...
}
}
After many considerations, this is how I solved it:
if (match !== null && match.length > 0) {
var parentNode = node.parentNode;
var nodes = [];
var textString = value;
var i = 0;
for (let entry of match) {
var startOffset = node.nodeValue.indexOf(entry[0]);
var endOffset = startOffset + entry[0].length;
var length = startOffset - i;
if (length > 0) {
var str = textString.substr(i, length);
var textNode = document.createTextNode(str);
parentNode.insertBefore(textNode, node);
}
var newNode2 = document.createElement("span");
newNode2.setAttribute("ko-text", entry[1]);
parentNode.insertBefore(newNode2, node);
nodes.push(newNode2);
i = endOffset;
}
var length = textString.length - i;
if (length > 0) {
var str = textString.substr(i, length);
var textNode = document.createTextNode(str);
parentNode.insertBefore(textNode, node);
}
parentNode.removeChild(node);
return nodes;
}
It can certainly be improved.
var textNodesWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
var node;
while (node = textNodesWalker.nextNode()) {
var newNodes = [];
var execResult = MY_REG_EXP.exec(node.nodeValue);
var start = 0;
while (execResult) {
newNodes.push(document.createTextNode(node.nodeValue.substring(start, execResult.index)));
var generatedElement = ...; // create element (document.createElement) based on execResult
newNodes.push(generatedElement);
start = execResult.index + execResult[0].length;
execResult = MY_REG_EXP.exec(node.nodeValue);
}
if (newNodes.length == 0) {
continue;
}
newNodes.push(document.createTextNode(node.nodeValue.substring(start, node.nodeValue.length)));
for (var i=0; i<newNodes.length; i++) {
node.parentNode.insertBefore(newNodes[i], node)
}
node.parentNode.removeChild(node);
}
Let's assume we have pattern /asdf/g than need to be made bold. My code will transform following html:
<p>Begin asdf End</p>
to
<p>Begin <b>asdf</b> End</p>
I'm ultimately wanting to create a jQuery plugin that loops through the top level of the DOM and adds elements to an object until it gets to a heading, at which point it pushes an object with the text of that heading.
The following sibling elements would be added to this new object until a new heading is encountered — if the heading is at the same level, a new object is created as a sibling of the parent object as before, and DOM siblings are added to that instead of the first object; if the heading is at a lower level, that's added as a child of the first object, and sibling DOM elements are added as children to that heading object; if it's a higher level headline, a new object is added one level above the last heading object and the cycle continues.
Example:
<p>wooo</p>
<h1>stuff</h1>
<p>stuff</p>
<p>more stuff</p>
<h2>yet more stuff</h2>
<p>still more stuff</p>
<h3>even still more stuff</h3>
<p>yep — stuff!</p>
<h1>still yet more stuff</h1>
<p>stuff stuff stuff</p>
<p>stuff stuff stuffarino</p>
Becomes...
{
'p_wooo': HTMLElementObject,
'h1_stuff': {
'p_stuff': HTMLElementObject,
'p_more_stuff': HTMLElementObject,
'h2_yet_more_stuff': {
'p_still_more_stuff': HTMLElementObject,
'h3_even_still_more_stuff': {
'p_yep_stuff': HTMLElementObject,
}
},
},
'h1_still_yet_more_stuff': {
'p_stuff_stuff_stuff': HTMLElementObject,
'p_stuff_stuff_stuffarino': HTMLElementObject
{
}
Here's what I have so far:
var root = $(res)
.filter('#contents')
.children()
.not('style'); // Don't need no stylesheets hurr!
var sections = root.filter('h1');
var outline = {};
for (var i = 0; i < sections.length; i++) {
var children;
if (i+1 <= sections.length) {
children = root.filter(sections[i]).after().nextUntil(sections[i+1]).filter(function(){return $(this).text().trim() !== '';});
}
var slug = getSlug($(sections[i]).text(), {separator: '_'});
outline[slug] = children;
}
console.dir(outline);
Alas, it only works for H1s. How would I turn this into a recursive function that adds H2-H6s?
I'll start you with an example that traverses the nodes and adds them all into the same tree object. It should be fairly easy to figure out the rest from here:
JSBin: http://jsbin.com/bixekutuhe/1/edit?html,js,output
// Helpers
function isNode(el) { return el && el.nodeType === 1; }
function tag(el) { return el.tagName.toLowerCase(); }
var tree = {}, key;
var node = document.body.firstElementChild;
while (isNode(node) && tag(node) !== 'script') { // add blacklists or whitelists that you might need
key = node.textContent;
tree[node.tagName.toLowerCase() + '_' +key.split(' ').join('_')] = node;
node = node.nextElementSibling; // move to next element
}
console.log(tree);
Update
Try the following example instead:
var tree = {};
var currentTree = tree, tagName, key;
var node = document.body.firstElementChild;
function isNode(el) { return el && el.nodeType === 1; }
function tag(el) { return el.tagName.toLowerCase(); }
while (isNode(node)) {
tagName = tag(node);
key = tagName + '_' + node.textContent.trim().split(' ').join('_');
switch(tagName) {
case 'h1':
case 'h2':
case 'h3':
case 'h4':
case 'h5':
case 'h6':
if (tagName === 'h1') {
currentTree = tree[key] = {};
} else {
currentTree = currentTree[key] = {};
}
break;
default:
currentTree[key] = node;
break;
}
// Move to the next element
node = node.nextElementSibling;
}
console.log(tree);
Ideally, I would like them all running, but when I have the second and/or third one active, Script 1 fails. (It just does nothing)
I wonder, is it possible to merge them into one script? Would that solve the problem? (I am tempted to try cutting and pasting them into one script just to see what happens)
Script 1 (the reloader)
(function () {
"use strict";
function walkTheDOM(node, func) {
if (node && node.nodeType) {
if (typeof func === "function") {
func(node);
}
node = node.firstChild;
while (node) {
walkTheDOM(node, func);
node = node.nextSibling;
}
}
}
function filterElementsByContains(elements, string) {
var toStringFN = {}.toString,
text = toStringFN.call(elements),
result,
length,
i,
element;
if (text !== "[object NodeList]" && text !== "[object Array]" && !($() instanceof jQuery)) {
return result;
}
result = [];
if (typeof string === "string") {
string = new RegExp("^" + string + "$");
} else if (toStringFN.call(string) !== "[object RegExp]") {
return result;
}
function getText(node) {
if (node.nodeType === 3) {
text += node.nodeValue;
}
}
length = elements.length;
i = 0;
while (i < length) {
text = "";
element = elements[i];
walkTheDOM(element, getText);
if (string.test(text)) {
result.push(element);
}
i += 1;
}
return result;
}
if(!filterElementsByContains([document.getElementsByTagName("table")[0]], /We are proud to announce that the November discounts have been chosen/).length) {
location.reload();
}
}());
Script 2 (Jump to last sheet, if it's multi sheet)
function getPreviousLink(){
var nextLink = document.getElementById('pagination-next-link');
var links = document.getElementsByClassName('v_page_nav')[0].getElementsByTagName("a");
for(var i=0; i < links.length; i++){
if(links[i] == nextLink) { return links[i-1]; }
}
}
var link = getPreviousLink();
link.target="_blank";
link.click();
Script 3 (open previous sheet, if there is one)
var link = document.getElementById('pagination-prev-link');
link.target="_blank";
link.click();
If the second 2 scripts aren't changing anything that would cause your first script to break (like something the first script looks for gets removed by script 2).... I would suggest going into 'manage scripts' and changing the order they run in... sometimes that can fix issues like this.
Scenario
I am trying to develop a Javascript text highlight function.
It receives in input a text to search inside, an array of tokens to be searched, a class to wrap the found matches:
var fmk = fmk || {};
fmk.highlight = function (target, tokens, cls) {
var token, re;
if (tokens.length > 0) {
token = tokens.pop();
re = new RegExp(token, "gi");
return this.highlight(
target.replace(re, function (matched) {
return "<span class=\"" + cls + "\">" + matched + "</span>";
}), tokens, cls);
}
else { return target; }
};
It is based on a recursive replace that wraps a <span> tag around the found matches.
JsFiddle demo.
Issues
if there are two tokens, and the latter is a substring of the former then only the latter token will be highligthed. In the jsFiddle example try these tokens: 'ab b'.
if the tokens contains a substring of the wrapper sequence (i.e. <span class="[className]"></span>) and another matching token, then the highlight fails and returns a dirty result. In the jsFiddle example try these tokens: 'red ab'.
Note that single character tokens are admitted in the actual application.
Questions
How to avoid these errors? I figured out these approaches:
To pre-process the tokens, removing the tokens that are substrings of others. Disadvantages: it requires O(n^2) searches in the pre-processing phase in case of n tokens; good matches are cut off.
To pre-process the matches BEFORE applying the wrapper, in order to cut off only the substrings matches. Disadvantages: again, further computation required. Anyway, I don't know where to start from implement this inside the replace callback function.
I think the way to handle this is to loop through all descendants of an element, check if it's a text node, and replace the appropriate content wrapped with a span/class.
var MyApp = {};
MyApp.highlighter = (function () {
"use strict";
var checkAndReplace, func,
id = {
container: "container",
tokens: "tokens",
all: "all",
token: "token",
className: "className",
sensitiveSearch: "sensitiveSearch"
};
checkAndReplace = function (node, tokenArr, classNameAll, sensitiveSearchAll) {
var nodeVal = node.nodeValue, parentNode = node.parentNode,
i, j, curToken, myToken, myClassName, mySensitiveSearch,
finalClassName, finalSensitiveSearch,
foundIndex, begin, matched, end,
textNode, span;
for (i = 0, j = tokenArr.length; i < j; i++) {
curToken = tokenArr[i];
myToken = curToken[id.token];
myClassName = curToken[id.className];
mySensitiveSearch = curToken[id.sensitiveSearch];
finalClassName = (classNameAll ? myClassName + " " + classNameAll : myClassName);
finalSensitiveSearch = (typeof sensitiveSearchAll !== "undefined" ? sensitiveSearchAll : mySensitiveSearch);
if (finalSensitiveSearch) {
foundIndex = nodeVal.indexOf(myToken);
} else {
foundIndex = nodeVal.toLowerCase().indexOf(myToken.toLowerCase());
}
if (foundIndex > -1) {
begin = nodeVal.substring(0, foundIndex);
matched = nodeVal.substr(foundIndex, myToken.length);
end = nodeVal.substring(foundIndex + myToken.length, nodeVal.length);
if (begin) {
textNode = document.createTextNode(begin);
parentNode.insertBefore(textNode, node);
}
span = document.createElement("span");
span.className += finalClassName;
span.appendChild(document.createTextNode(matched));
parentNode.insertBefore(span, node);
if (end) {
textNode = document.createTextNode(end);
parentNode.insertBefore(textNode, node);
}
parentNode.removeChild(node);
}
}
};
func = function (options) {
var iterator,
tokens = options[id.tokens],
allClassName = options[id.all][id.className],
allSensitiveSearch = options[id.all][id.sensitiveSearch];
iterator = function (p) {
var children = Array.prototype.slice.call(p.childNodes),
i, cur;
if (children.length) {
for (i = 0; i < children.length; i++) {
cur = children[i];
if (cur.nodeType === 3) {
checkAndReplace(cur, tokens, allClassName, allSensitiveSearch);
} else if (cur.nodeType === 1) {
iterator(cur);
}
}
}
};
iterator(options[id.container]);
};
return func;
})();
window.onload = function () {
var container = document.getElementById("container");
MyApp.highlighter({
container: container,
all: {
className: "highlighter"
},
tokens: [{
token: "sd",
className: "highlight-sd",
sensitiveSearch: false
}, {
token: "SA",
className: "highlight-SA",
sensitiveSearch: true
}]
});
};
DEMO: http://jsfiddle.net/UWQ6r/1/
I set it up so you can change the values in id so that you can use different keys in the {} passed to highlighter.
The two settings in the all object refer to a class being added no matter what, as well as a case sensitive search override. For each token, you specify the token, class, and whether the match should be case sensitive.
References:
nodeType: https://developer.mozilla.org/en-US/docs/DOM/Node.nodeType
childNodes: https://developer.mozilla.org/en-US/docs/DOM/Node.childNodes
substr: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substr
substring: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/String/substring
insertBefore: https://developer.mozilla.org/en-US/docs/DOM/Node.insertBefore
This seems to work for me:
(line 17 in your JsFiddle demo)
Issue 1: var tokens = [['ab','b'].join("|")];
Issue 2: var tokens = ['<span'.replace(/</g,"<")];
All together, then:
var tokens = [[..my tokens..].sort().join("|").replace(/</g,"<")];
(by the way, I did test tokens such as '"', '"s' or 'span' and they seem to work fine. Also, I'm not sure why .sort() is important here but I left it in since I like to stay close to the original code.)
I am trying to implement my own getElementById() function in Javascript. My idea/algorithm goes like this:
function myGetElemById(id){
// rootNode I suppose will be the BODY tag.
rootElem = get elements by TAGNAME (rootNode);
elems = rootElems.getChildren();
for(i=0; i<elems.length; i++){
if(!elems[i].hasChildren()){
myGetElemById(elems[i]);
} else {
if(elems[i].id == id)
return elems[i];
else
return null;
}
}
}
Method 1:
function myGetElemById(id){
return document.getElementById(id);
}
Method 2:
function myGetElemById(id){
return window[id];
}
Method 3: (newer browsers)
function myGetElemById(id){
return document.querySelectorAll('#' + id);
}
DONE!
Okay, seriously:
function getById(id, parent, list){
parent = parent || document.body;
list = list || [];
var l, child, children = parent.children;
if(children){
l = children.length;
while(l--){
child = children[l];
if(child.id == id) list.push(child);
getById(id, child, list);
}
}
return list;
}
Check out this feature and maybe you can get ideas
function getElementsStartsWithId( id ) {
var children = document.body.getElementsByTagName('*');
var elements = [], child;
for (var i = 0, length = children.length; i < length; i++) {
child = children[i];
if (child.id.substr(0, id.length) == id)
elements.push(child);
}
return elements;
}
first, you must deal with the elements has children, call myGetElemById() and choose to return or not to return, depend on the result. like this
...
if(!elems[i].hasChildren()){
var result = myGetElemById(elems[i]);
if (result != null)
return result;
} else {
...
second why iterate over all the elements of the dom? the native function is much more faster.
Custom get element by ID method using BFS:
function myGetElementById(id, root){
let queue = []; // Using array here but linkedList is more performant in both time and space complexity
queue.push(root);
let currentNode;
while(queue.length){
currentNode = queue.shift();
if(currentNode.id === id){
return currentNode;
}
queue.push(...currentNode.children);
}
return false;
}