Highly inefficient
The code is highly inefficient, wasting CPU cycles and memory due to poor use of Javascript API's. It is also badly formatted and thus hard to read and follow and poor naming also made working out what you were doing difficult.
Code Points.
- Better naming alround. Example, you have two functions
transformLinkToTextOnly
and transformLinksToTextOnly
Now I might be a bit blind but without spaces its not at all clear that these are two separate functions. If you consider the context of the functions and encapsulate them you can reduce the complex names to transformLink
and transformLinks
making the plural stand out, the rest is inferred due to context.
- Indent chained functions.
- Use semicolons to end lines. There are many arguments for and against using semicolons. Semicolons are required by the language, and are inserted automatically when javascript is parsed, however there are many edge cases that most people are unaware of and can catch you out. The rule of thumb is "Can you list all edge cases where semicolon insertion can cause problems?" if not use them. It is even more important that you are consistent. Even if you know all the rules, using semicolons in some places and not in others breaks the style rule "Be consistent".
- Avoid single use variable declarations.
- If you use terneries in multi expression lines, put them inside
(...)
for clarity. See example A
- Know your javascript. It is important that you are thoroughly familiar with the language you write in. You need to regularly review the JS reference and know the most concise and performant forms to achieve what you desire. Your code is very GC (garbage collection) unfriendly creating many arrays and thrashing memory and slowuing down the code, all of which can be avoided . See example B
Example A
return withReaddedOpeningTags
.reduce((prev, curr) => [
...prev,
curr.indexOf('<a') > -1 ?
splitByLinkEnds(curr) : curr
], []).flat()
Better syntax written as
return withReaddedOpeningTags
.reduce((prev, curr) =>
[...prev, (curr.indexOf('<a') > -1 ? splitByLinkEnds(curr) : curr)]
, [])
.flat();
As I am at this function I will point out that for every iteration you make a new copy of the array, add it as another array, and in the end you are forced to flatten the result to get what you want.
The same can be done with.
return withReaddedOpeningTags
.map(item => item.indexOf('<a') > -1 ? splitByLinkEnds(item) : item);
or
return withReaddedOpeningTags
.map(item => item.includes('<a') ? splitByLinkEnds(item) : item);
Example B
An example function splitByLinkEnds
create 5 arrays
const splitByLinkEnds = html => {
const split = html.split('</a>'); // array 1
const allButLast = split.slice(0, -1); // array 2
const last = split.slice(-1)[0]; // creates array 3 an uses first item
return [
...allButLast.map(x => `${x}</a>`), // map creates an array 4 that is then
// copied to the return array 5
last
];
}
It can be done using only one new array.
const splitEnds = markup => {
const parts = markup.split("</a>");
const end = parts.pop();
parts.forEach((x, i) => parts[i] = x + "</a>");
parts.push(end);
return parts;
}
or as
const splitEnds = markup => {
return markup.split("</a>").forEach((x, i, a) => a[i] = x + (i < a.length - 1 ? "</a>" : ""));
}
Rewrites
I have include 4 ways of achieving the same result. Javascript is very expressive so there is no single right way to do anything.
Rewrite using only strings and arrays
This method does not require the DOM and splits the markup at anchor boundaries creating an array of anchor descriptions which is then used to replace the anchors in the original markup.
function replaceAnchors(markup) {
function findLinks(markup) {
return markup.substring(markup.indexOf("<a") + 2).split("<a")
.map(markup => ({
markup : "<a" + markup.split("</a>")[0] + "</a>",
anchor : markup.split(">")[0],
href : markup.split("href=\"")[1].split("\"")[0],
text : markup.split(">")[1].split("</a")[0],
}));
}
findLinks(markup).forEach(link => {
markup = markup.replace(link.markup,`<span${link.anchor}>${link.text}(${link.href})</span>`);
});
return markup;
}
Second version does away with the intermediate array and does it in one pass.
function replaceAnchors(markup) {
markup.substring(markup.indexOf("<a") + 2).split("<a")
.forEach(a => {
const href = a.split("href=\"")[1].split("\"")[0];
const text = a.split(">")[1].split("</a")[0];
markup = markup.replace(
`<a${a.split("</a>")[0]}</a>`,
`<span${a.split(">")[0]}>${text}(${href})</span>`
);
});
return markup;
}
Rewrite with memory and performance in mind.
This uses named properties to determine what to do with each character in turn. If a match[current]
is found then actions[current].add
and actions[current].action
are performed, each action sets up the next expected match and actions by setting the string current
to the correct name. It is very fast and has a little room to go even faster. It is also very memory efficient
function replaceAnchors(markup) {
const match = {
get anchorStart() { return markup[idx] === "<" && markup[idx+1] === "a" },
get hrefStart() { return markup.substring(idx,idx + 6) === "href=\"" },
get hrefEnd() { return markup[idx] === "\"" },
get tagClose() { return markup[idx] === "<" && markup[idx+1] === "/" && markup[idx+2] === "a" },
};
const actions = {
anchorStart: {
add() { idx += 2; return "<span" },
action() { current = "hrefStart" }
},
hrefStart: {
add() { idx += 6; return "href=\""},
action() { hrefStart = idx; current = "hrefEnd" }
},
hrefEnd: {
add() { idx += 1; return "\""},
action() { href = markup.substring(hrefStart, idx -1); current = "tagClose" }
},
tagClose: {
add() { idx += 4; return "(" + href + ")</span>" },
action() { current = "anchorStart" }
},
}
var result = "", idx = 0, hrefStart, href, current = "anchorStart";
while (idx < markup.length) {
if (match[current]) {
result += searchs[current].add();
searchs[current].action();
} else {
result += markup[idx++];
}
}
return result;
}
Rewrite using the DOM
This method uses the Markup parser to help locate anchor tags and then just replaces the start and end tag with span and the additional href
function replaceAnchors(markup) {
const nodes = document.createElement("span");
nodes.innerHTML = markup;
for (const a of nodes.querySelectorAll("a")) {
const anchor = a.outerHTML;
markup = markup.replace(anchor, anchor
.replace("</a>",`(${a.getAttribute("href")})</span>`)
.replace("<a","<span")
);
}
return markup;
}