Reputation: 1
hoping to avoid repeatedly typing references to anchors by hand, I tried to come up with a way to make any occurrence of given terms into automagically linked references to a same-named anchor, like turn 'foo' into <a href="#foo">foo</a>
, 'bar' into <a href="#bar">bar</a>
, and so on.
I can't, however, seem to get my clunky approach to skip already linked occurrences (or other elements like style, script, etc). Positive selection (like
nodeList = document.querySelectorAll(".entry-content a");
) works just fine, but exclusion (like e.g.
nodeList = document.querySelectorAll(".entry-content:not(a, style, script)");
eludes me. I sifted thru quite a lot of questions that looked similar, already, but could adapt none for my stubborn problem :/ so, I must definitely be doing something wrong.
Your help figuring this out is much appreciated. Here's where I'm at right now:
function rep() {
const nodeList = document.querySelectorAll(".entry-content:not(a, style, script)");
for (let i = 0; i < nodeList.length; i++) {
nodeList[i].innerHTML = nodeList[i].innerHTML.replace(/foo/g, '<a href="#foo" style="background: lime;">REPLACED FOO</a>');
}
}
But this just blatantly replaces every occurrence of 'foo' inside my class="entry-content", regardless of element type it appears in (instead of disregarding a and style and script elements).
Thank you for your look at this. Cheers -
Upvotes: 0
Views: 217
Reputation: 370979
:not
only accepts "simple selectors" - selectors with only one component to them. a, style, script
is not a simple selector, so :not(a, style, script)
doesn't produce the desired results. You could put the logic to exclude those tags in the JavaScript instead of the selector.
But that's not enough. Some of the elements you don't want to match are descendants of the .entry-content
elements. For example, a <p class="entry-content">
will not be excluded from being a match just because it has an <a>
descendant. So just matching .entry-content
elements and replacing won't be enough. You'll need some different logic to identify text which has a .entry-content
ancestor and also doesn't have a blacklisted tag as an ancestor.
One possibility would be to iterate over text nodes and check if their parent element has a .closest
element that matches, and doesn't have a .closest
element in the blacklist. The replacement of text nodes with a varying number of possibly non-text nodes is pretty cumbersome, unfortunately. Better to avoid assigning to .innerHTML
- that'll corrupt references that JavaScript scripts may have to elements inside the container that gets altered.
// https://stackoverflow.com/q/2579666
function nativeTreeWalker() {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT,
null,
false
);
let node;
const textNodes = [];
while(node = walker.nextNode()) {
textNodes.push(node);
}
return textNodes;
}
function rep() {
for (const node of [...nativeTreeWalker()]) {
const parent = node.parentElement;
if (!node.textContent.includes('foo') || !parent.closest('.entry-content') || parent.closest('a, style, script')) {
continue;
}
// Degenerate case
if (parent.matches('textarea')) {
// In modern environments, use `.replaceAll` instead of regex
parent.textContent = parent.textContent.replaceAll('foo', '<a href="#foo" style="background: lime;">REPLACED FOO</a>');
continue;
}
// At this point, we know this node needs to be replaced.
// Can't just use parent.innerHTML = parent.innerHTML.replace
// because other children of the parent (siblings of this node) may be on the blacklist
// Use a DocumentFragment
// so the new nodes can be inserted at the right position
// all at once at the end
const newNodesFragment = new DocumentFragment();
for (const match of node.textContent.match(/foo|((?!foo).)+/g)) {
if (match !== 'foo') {
newNodesFragment.append(match);
} else {
newNodesFragment.append(
Object.assign(
document.createElement('a'),
{
href: '#foo',
style: 'background: lime',
textContent: 'REPLACED FOO'
}
)
);
}
}
// Insert the new nodes
parent.insertBefore(newNodesFragment, node);
// Remove the original text node
parent.removeChild(node);
}
}
.red {
background: red;
}
<h1>A Foo is not a foo and a Bar is not a bar</h1>
<p style="color:gray;"> this p has <b>no class</b>, nothing should happen here. Hi, I'm a foo. I'm a bar. Hi, I'm <a href="some link.htm">an already <b>linked foo</b></a>. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm <a href="some link.htm"
target="_blank">an already <b>linked bar</b></a>. Hi, I'm a foo. I'm a bar.</p>
<p class="entry-content">This p <b>has class .entry-content</b>. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm <a href="some link.htm" target="_blank">.ean already <b>linked foo - should not be altered</b></a>.
I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. <span class="red">Hi, I'm a foo</span>. I'm a bar. Hi, I'm a <span><b>foo in a span</b></span>. I'm a bar. Hi, I'm <a href="some link.htm">an already <b>linked bar</b></a>.
Hi, I'm a foo. I'm a bar. <textarea>Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar. Hi, I'm a foo. I'm a bar.</textarea> </p>
<button onclick="rep()">do rep()</button>
Upvotes: 2
Reputation: 18585
You want to create a class to make new anchor elements.
class anchor {
constructor(text) {
let a = document.createElement("a");
let t = document.createTextNode(text);
a.href = text;
a.appendChild(t);
return a;
}
}
document.body.appendChild(new anchor("foo"));
document.body.appendChild(new anchor("bar"));
a {
margin:24px;
}
Upvotes: 0