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>
Related
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
I have a function like this that's provided by a user:
function replace_function(string) {
return string.replace(/:smile:/g, '⻇')
.replace(/(foo|bar|baz)/g, 'text_$1');
}
and I have input string like this:
var input = 'foo bar :smile: xxxx';
and I have a number from 0 to length of the input string that I use to do substring to split the string.
I need to find number (position) that will match output string after replacement so the split is in the same visual place. The split is just for visualization I only need the number.
The output string can have the same length, this is only for the case when length of input and output is different (like width provided function and input string)
function replace_function(string) {
return string.replace(/:smile:/g, '⻇')
.replace(/(foo|bar|baz)/g, 'text_$1');
}
var textarea = document.querySelector('textarea');
var pre = document.querySelector('pre');
function split() {
var input = textarea.value;
var output = replace_function(input);
// find position for output
var position = textarea.selectionStart;
var split = [
output.substring(0, position),
output.substring(position)
];
pre.innerHTML = JSON.stringify(split);
}
textarea.addEventListener('click', split);
<textarea>xxx foo xxx bar xxx :smile: xxxx</textarea>
<pre></pre>
when you click in the middle of the word that get replaced the position/split need to be after the output word. If you click before, between or after the word the position need to be in the same place (the position in each case will be different to match the correct place)
UPDATE: here is my code that work for :smile: only input, but there is need to be text before :smile: (input = ":smile: asdas" and position in the middle of smile and the position is off)
it work for foo replaced by text_foo but not in the case when there is :smile: before foo (input "asd :smile: asd foo").
var get_position = (function() {
function common_string(formatted, normal) {
function longer(str) {
return found && length(str) > length(found) || !found;
}
var formatted_len = length(formatted);
var normal_len = length(normal);
var found;
for (var i = normal_len; i > 0; i--) {
var test_normal = normal.substring(0, i);
var formatted_normal = replace_function(test_normal);
for (var j = formatted_len; j > 0; j--) {
var test_formatted = formatted.substring(0, j);
if (test_formatted === formatted_normal &&
longer(test_normal)) {
found = test_normal;
}
}
}
return found || '';
}
function index_after_formatting(position, command) {
var start = position === 0 ? 0 : position - 1;
var command_len = length(command);
for (var i = start; i < command_len; ++i) {
var substr = command.substring(0, i);
var next_substr = command.substring(0, i + 1);
var formatted_substr = replace_function(substr);
var formatted_next = replace_function(next_substr);
var substr_len = length(formatted_substr);
var next_len = length(formatted_next);
var test_diff = Math.abs(next_len - substr_len);
if (test_diff > 1) {
console.log('return ' + i);
return i;
}
}
}
return function get_formatted_position(position, command) {
var formatted_position = position;
var string = replace_function(command);
var len = length(string);
var command_len = length(command);
if (len !== command_len) {
var orig_sub = command.substring(0, position);
var orig_len = length(orig_sub);
var sub = replace_function(orig_sub);
var sub_len = length(sub);
var diff = Math.abs(orig_len - sub_len);
if (false && orig_len > sub_len) {
formatted_position -= diff;
} else if (false && orig_len < sub_len) {
formatted_position += diff;
} else {
var index = index_after_formatting(position, command);
var to_end = command.substring(0, index + 1);
//formatted_position -= length(to_end) - orig_len;
formatted_position -= orig_len - sub_len;
if (orig_sub && orig_sub !== to_end) {
var formatted_to_end = replace_function(to_end);
var common = common_string(formatted_to_end, orig_sub);
var re = new RegExp('^' + common);
var before_end = orig_sub.replace(re, '');
var to_end_rest = to_end.replace(re, '');
var to_end_rest_len = length(replace_function(to_end_rest));
if (before_end && orig_sub !== before_end) {
var commnon_len = length(replace_function(common));
formatted_position = position - length(before_end) + to_end_rest_len;
}
}
}
if (formatted_position > len) {
formatted_position = len;
} else if (formatted_position < 0) {
formatted_position = 0;
}
}
return formatted_position;
};
})();
function length(str) {
return str.length;
}
function replace_function(string) {
return string.replace(/:smile:/g, '⻇')
.replace(/(foo|bar|baz)/g, 'text_$1');
}
var textarea = document.querySelector('textarea');
var pre = document.querySelector('pre');
function split() {
var input = textarea.value;
var output = replace_function(input);
// find position for output
var position = get_position(textarea.selectionStart, input);
var split = [
output.substring(0, position),
output.substring(position)
];
pre.innerHTML = JSON.stringify(split);
}
textarea.addEventListener('click', split);
<textarea>xxxx :smile: xxxx :smile: xxx :smile:</textarea>
<pre></pre>
To do this, you'll have to do the replace operation yourself with a RegExp#exec loop, and keep track of how the replacements affect the position, something along these lines (but this can probably be optimized):
function trackingReplace(rex, string, replacement, position) {
var newString = "";
var match;
var index = 0;
var repString;
var newPosition = position;
var start;
rex.lastIndex = 0; // Just to be sure
while (match = rex.exec(string)) {
// Add any of the original string we just skipped
if (rex.global) {
start = rex.lastIndex - match[0].length;
} else {
start = match.index;
rex.lastIndex = start + match[0].length;
}
if (index < start) {
newString += string.substring(index, start);
}
index = rex.lastIndex;
// Build the replacement string. This just handles $$ and $n,
// you may want to add handling for $`, $', and $&.
repString = replacement.replace(/\$(\$|\d)/g, function(m, c0) {
if (c0 == "$") return "$";
return match[c0];
});
// Add on the replacement
newString += repString;
// If the position is affected...
if (start < position) {
// ... update it:
if (rex.lastIndex < position) {
// It's after the replacement, move it
newPosition = Math.max(0, newPosition + repString.length - match[0].length);
} else {
// It's *in* the replacement, put it just after
newPosition += repString.length - (position - start);
}
}
// If the regular expression doesn't have the g flag, break here so
// we do just one replacement (and so we don't have an endless loop!)
if (!rex.global) {
break;
}
}
// Add on any trailing text in the string
if (index < string.length) {
newString += string.substring(index);
}
// Return the string and the updated position
return [newString, newPosition];
}
Here's a snippet showing us testing that with various positions:
function trackingReplace(rex, string, replacement, position) {
var newString = "";
var match;
var index = 0;
var repString;
var newPosition = position;
var start;
rex.lastIndex = 0; // Just to be sure
while (match = rex.exec(string)) {
// Add any of the original string we just skipped
if (rex.global) {
start = rex.lastIndex - match[0].length;
} else {
start = match.index;
rex.lastIndex = start + match[0].length;
}
if (index < start) {
newString += string.substring(index, start);
}
index = rex.lastIndex;
// Build the replacement string. This just handles $$ and $n,
// you may want to add handling for $`, $', and $&.
repString = replacement.replace(/\$(\$|\d)/g, function(m, c0) {
if (c0 == "$") return "$";
return match[c0];
});
// Add on the replacement
newString += repString;
// If the position is affected...
if (start < position) {
// ... update it:
if (rex.lastIndex < position) {
// It's after the replacement, move it
newPosition = Math.max(0, newPosition + repString.length - match[0].length);
} else {
// It's *in* the replacement, put it just after
newPosition += repString.length - (position - start);
}
}
// If the regular expression doesn't have the g flag, break here so
// we do just one replacement (and so we don't have an endless loop!)
if (!rex.global) {
break;
}
}
// Add on any trailing text in the string
if (index < string.length) {
newString += string.substring(index);
}
// Return the string and the updated position
return [newString, newPosition];
}
function show(str, pos) {
console.log(str.substring(0, pos) + "|" + str.substring(pos));
}
function test(rex, str, replacement, pos) {
show(str, pos);
var result = trackingReplace(rex, str, replacement, pos);
show(result[0], result[1]);
}
for (var n = 3; n < 22; ++n) {
if (n > 3) {
console.log("----");
}
test(/([f])([o])o/g, "test foo result foo x", "...$2...", n);
}
.as-console-wrapper {
max-height: 100% !important;
}
And here's your snippet updated to use it:
function trackingReplace(rex, string, replacement, position) {
var newString = "";
var match;
var index = 0;
var repString;
var newPosition = position;
var start;
rex.lastIndex = 0; // Just to be sure
while (match = rex.exec(string)) {
// Add any of the original string we just skipped
if (rex.global) {
start = rex.lastIndex - match[0].length;
} else {
start = match.index;
rex.lastIndex = start + match[0].length;
}
if (index < start) {
newString += string.substring(index, start);
}
index = rex.lastIndex;
// Build the replacement string. This just handles $$ and $n,
// you may want to add handling for $`, $', and $&.
repString = replacement.replace(/\$(\$|\d)/g, function(m, c0) {
if (c0 == "$") return "$";
return match[c0];
});
// Add on the replacement
newString += repString;
// If the position is affected...
if (start < position) {
// ... update it:
if (rex.lastIndex < position) {
// It's after the replacement, move it
newPosition = Math.max(0, newPosition + repString.length - match[0].length);
} else {
// It's *in* the replacement, put it just after
newPosition += repString.length - (position - start);
}
}
// If the regular expression doesn't have the g flag, break here so
// we do just one replacement (and so we don't have an endless loop!)
if (!rex.global) {
break;
}
}
// Add on any trailing text in the string
if (index < string.length) {
newString += string.substring(index);
}
// Return the string and the updated position
return [newString, newPosition];
}
function replace_function(string, position) {
var result = trackingReplace(/:smile:/g, string, '⻇', position);
result = trackingReplace(/(foo|bar|baz)/g, result[0], 'text_$1', result[1]);
return result;
}
var textarea = document.querySelector('textarea');
var pre = document.querySelector('pre');
function split() {
var position = textarea.selectionStart;
var result = replace_function(textarea.value, position);
var string = result[0];
position = result[1];
var split = [
string.substring(0, position),
string.substring(position)
];
pre.innerHTML = JSON.stringify(split);
}
textarea.addEventListener('click', split);
<textarea>:smile: foo</textarea>
<pre></pre>
I am currently using the code below to detect if a URL is pasted into a contenteditable div. If a URL is pasted, it will automatically be converted into a link (surrounded by a tags).
How would I change this so that if the user pastes an image URL, it would be converted to <img src="https://example.com/image.jpg"> whilst also converting non-image URL's to standard links (surrounded by a tags).
var saveSelection, restoreSelection;
if (window.getSelection && document.createRange) {
saveSelection = function(containerEl) {
var range = window.getSelection().getRangeAt(0);
var preSelectionRange = range.cloneRange();
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
var start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
}
};
} else if (document.selection) {
}
function createLink(matchedTextNode) {
var el = document.createElement("a");
el.href = matchedTextNode.data;
el.appendChild(matchedTextNode);
return el;
}
function shouldLinkifyContents(el) {
return el.tagName != "A";
}
function surroundInElement(el, regex, surrounderCreateFunc, shouldSurroundFunc) {
var child = el.lastChild;
while (child) {
if (child.nodeType == 1 && shouldSurroundFunc(el)) {
surroundInElement(child, regex, createLink, shouldSurroundFunc);
} else if (child.nodeType == 3) {
surroundMatchingText(child, regex, surrounderCreateFunc);
}
child = child.previousSibling;
}
}
function surroundMatchingText(textNode, regex, surrounderCreateFunc) {
var parent = textNode.parentNode;
var result, surroundingNode, matchedTextNode, matchLength, matchedText;
while ( textNode && (result = regex.exec(textNode.data)) ) {
matchedTextNode = textNode.splitText(result.index);
matchedText = result[0];
matchLength = matchedText.length;
textNode = (matchedTextNode.length > matchLength) ?
matchedTextNode.splitText(matchLength) : null;
surroundingNode = surrounderCreateFunc(matchedTextNode.cloneNode(true));
parent.insertBefore(surroundingNode, matchedTextNode);
parent.removeChild(matchedTextNode);
}
}
var textbox = $('.editable')[0];
var urlRegex = /http(s?):\/\/($|[^\s]+)/;
function updateLinks() {
var savedSelection = saveSelection(textbox);
surroundInElement(textbox, urlRegex, createLink, shouldLinkifyContents);
restoreSelection(textbox, savedSelection);
}
var $textbox = $(textbox);
$(document).ready(function () {
$textbox.focus();
var keyTimer = null, keyDelay = 1000;
$textbox.keyup(function() {
if (keyTimer) {
window.clearTimeout(keyTimer);
}
keyTimer = window.setTimeout(function() {
updateLinks();
keyTimer = null;
}, keyDelay);
});
});
Did you try to parse the pasted url, and search for ending extension (jpg,gif,png) ?
It should be simple, if the ending is matching one of those, then you wrap the url into an href propriety.
Did You wrote this code by yourself?
Here you can read about strings methods to do this:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String
I'm trying to pass the backreferences to a dynamically-created-function as a variable (so i could check if the backreferences is set and if not throw an error) but i can't find a solution for passing it. How can you make it work???
This is the code:
class regexMap {
constructor(map) {
this.map = map;
}
replace(str){
for (var i = 0; i < this.map.length; i++){
var regexp = new RegExp(this.map[i][0], 'ig');
str = str.replace(regexp, this.map[i][1].apply(this));
}
return str;
}
}
// EXAMPLE:
var map = [
[/FIND (.*)/g,function(){
var br = '$1'; // Don't work.
if(br != '' && br != undefined){
return 'find(\'$1\');'
} else {
console.error('Find requires a string');
return;
}
}],
];
console.log(new regexMap(map).replace("FIND This is a string\nFIND "));
Thanks!
The fucntion you pass into replace will receive the full match as its first argument and then additional arguments containing the contents of capture groups. So you can declare those in your function, then use the function directly in your regexMap#replace method. See *** comments:
class regexMap {
constructor(map) {
this.map = map;
}
replace(str){
for (var i = 0; i < this.map.length; i++){
var regexp = new RegExp(this.map[i][0], 'ig');
str = str.replace(regexp, this.map[i][1].bind(this)); // ***
}
return str;
}
}
// EXAMPLE:
var map = [
[/FIND (.*)/g,function(m, br){ // ***
if(br != '' && br != undefined){
return 'find(\'' + br + '\');'
} else {
console.error('Find requires a string');
return;
}
}],
];
console.log(new regexMap(map).replace("FIND This is a string\nFIND "));
I have a bit of javascript code to find and replace text into an image. I then gather the font size of the original text and use that to set the size of the new image.
Problem is, I keep getting the error: Could not convert JavaScript argument arg 0 [nsIDOMWindow.getComputedStyle]
Code:
function findAndReplace(searchText, replacement, searchNode) {
if (!searchText || typeof replacement === 'undefined') {
// Throw error here if you want...
return;
}
var regex = typeof searchText === 'string' ?
new RegExp(searchText, 'g') : searchText,
childNodes = (searchNode || $("body").get(0)).childNodes,
excludes = 'html,head,style,title,link,meta,script,object,iframe';
var cnLength = childNodes.length;
while (cnLength--) {
var currentNode = childNodes[cnLength];
if (currentNode.nodeType === 1 &&
(excludes + ',').indexOf(currentNode.nodeName.toLowerCase() + ',') === -1) {
arguments.callee(searchText, replacement, currentNode);
}
if (currentNode.nodeType !== 3 || !regex.test(currentNode.data) ) {
continue;
}
var parent = currentNode.parentNode;
var frag = (function(){
var html = currentNode.data.replace(regex, replacement);
var wrap = document.createElement('div');
var frag = document.createDocumentFragment();
wrap.innerHTML = html;
while (wrap.firstChild) {
frag.appendChild(wrap.firstChild);
}
console.log(currentNode);
var jQNode = $(currentNode);
console.log("yay");
// var fontSize = jQNode.css('font-size');
if (!currentNode || currentNode == document) currentNode = document.body
var fontSize = getStyle(currentNode, 'font-size');
console.log("tast");
var heightPixels = fontSizeToPixels(fontSize);
$(".InLogo",frag).each(function(){
$(this).css("height", heightPixels+"px");
});
return frag;
})();
parent.insertBefore(frag, currentNode);
parent.removeChild(currentNode);
}
}
function getStyle(el,styleProp) {
var camelize = function (str) {
return str.replace(/\-(\w)/g, function(str, letter){
return letter.toUpperCase();
});
};
if (el.currentStyle) {
return el.currentStyle[camelize(styleProp)];
} else if (document.defaultView && document.defaultView.getComputedStyle) {
return document.defaultView.getComputedStyle(el,null)
.getPropertyValue(styleProp);
} else {
return el.style[camelize(styleProp)];
}
}
The error occurs at this line return document.defaultView.getComputedStyle(el,null).getPropertyValue(styleProp); of getStyle()
something.childNodes includes textNodes as well as Elements, and that's a problem for the getStyle() function.
Nodes don't have a style (Elements do), so who knows what will happen when you feed getStyle something that has .data; a plain Node.
Check for the existence of style to avoid the run-time error:
FIX:
var fontSize = currentNode.style ? getStyle(currentNode, 'font-size') : 0;