Need help to improve a code

Need help to improve a code

What this code here does is that it gets a html, let's say, a banner, and inserts it after a element tag and after x number of words, but it differs what are html tags from what is text, this way, the code won't strip any html tags. In other words, this jquery code is supposed to get a banner and place it on the middle of a text (Blog post for example) without breaking the text's html. This code works, but it needs some improvements.
For example, if I specify it to insert the html after 300 words, but there is a tag (Div, h1, p, whatever) after the word 200 (At position 201), it simply won't show any result.

JS Fiddle demo.


Here are the problems that I'm facing:

  • - It requires that all the words are held in a single text node; it doesn't even attempt to count words that might begin in one element and end inside of a sibling-element.
  • - It doesn't allow – in its current implementation – any way of inserting non-text into the created-element (although this could be allowed via the use of elem.innerHTML in place of elem.textContent), but it does return the created-element to the calling context so it can be cached or chained, which allows that created-element to be manipulated in some ways.
  • - Some of the checks, if not most, are profoundly naive; and would benefit from extension to account for your own particular edge-cases.

The part responsible for getting the element is:

  1. // specifying the node into which the element
  2.     // should inserted:
  3.         'node': document.querySelector('div > div')


The code:

  1. // a simple utility function to get only the actual words
  2. // from the supplied textNode (though this should work for
  3. // elements also):
  4. function getActualWords(node) {

  5.     // gets the textContent of the node,
  6.     // splits that string on one-or-more ('+')
  7.     // white-space characters ('\s');
  8.     // filters the array returned by split():
  9.     return node.textContent.split(/\s+/).filter(function (word) {
  10.         // word is the current array-element
  11.         // (a 'word') in the array over
  12.         // which we're iterating using
  13.         // Array.prototype.filter();
  14.         // here if the word, with leading
  15.         // and trailing white-space removed
  16.         // (using String.prototype.trim())
  17.         // has a length greater than 0
  18.         // (a falsey value) the word is kept
  19.         // in the array returned by filter:
  20.         return word.trim().length;

  21.         // note that negative numbers are
  22.         // also truthy, but no string can
  23.         // have a negative length; so the
  24.         // comparison is effectively, if
  25.         // not explicitly 'greater than zero'
  26.         // rather than simply 'not-zero'
  27.     });
  28. }

  29. // named function to insert the specified
  30. // element after the nth word:
  31. function insertElemAfterNthWord(opts) {

  32.     // defining the defaults for the function
  33.     // (which can be overridden via the opts
  34.     // Object):
  35.     var defaults = {

  36.         // the word after-which to insert the
  37.         // the new element:
  38.         'nth': 5,

  39.         // the text of the new element:
  40.             'elemText': 'new element',

  41.         // the type of element (note no '<' or '>'):
  42.             'elemTag': 'div'
  43.     };

  44.     // iterating over the supplied opts Object to update
  45.     // the defaults with the user-supplied options using
  46.     // for...in loop:
  47.     for (var prop in opts) {

  48.         // if the opts Object has a property and
  49.         // that property is not inherited from the
  50.         // prototype chain:
  51.         if (opts.hasOwnProperty(prop)) {

  52.             // we set the defaults property
  53.             // to the property-value held
  54.             // in the opts Object:
  55.             defaults[prop] = opts[prop];
  56.         }
  57.     }

  58.     // aliasing the defaults object (simply to save
  59.     // typing; this is not essential):
  60.     var d = defaults,

  61.         // ensuring that the supplied string,
  62.         // specifying the element-type has no
  63.         // '<' or '>' characters (to ensure validty
  64.         // this should be extended further to
  65.         // ensure only alphabetical characters are kept):
  66.         tag = d.elemTag.replace(/<|>/g, ''),

  67.         // creating the new element:
  68.         elem = document.createElement(tag);

  69.     // setting the textContent of the new element:
  70.     elem.textContent = d.elemText;

  71.     // ensuring that the d.nth variable is
  72.     // a number, not a string, in base-10:
  73.     d.nth = parseInt(d.nth, 10);

  74.     // if a node was specified:
  75.     if (d.node) {

  76.         // setting the 'n' variable to hold
  77.         // to the firstChild of the d.node:
  78.         var n = d.node.firstChild,

  79.             // using the utility function (above)
  80.             // to get an Array of only the actual 
  81.             // words held in the node:
  82.             words = getActualWords(n),

  83.             // getting the number of words held
  84.             // in the Array of words:
  85.             wordCount = words.length;

  86.         // while (n.nodeType is not a textNode OR
  87.         // d.nth is a greater number than the number
  88.         // of words in the node) AND the node has
  89.         // a following sibling node:
  90.         while ((n.nodeType !== 3 || d.nth > wordCount) && n.nextSibling) {

  91.             // we update n to the next-sibling:
  92.             n = n.nextSibling;

  93.             // we get an array of words from
  94.             // newly-assigned node:
  95.             words = getActualWords(n);

  96.             // we update the wordCount, in
  97.             // order to progress through:
  98.             wordCount = words.length;
  99.         }

  100.         // if the number of words is less than
  101.         // the nth word after which we want to
  102.         // insert the element, we return from
  103.         // the function (doing nothing):
  104.         if (getActualWords(n).length < d.nth) {
  105.             return;

  106.         // otherwise:
  107.         } else {

  108.             // again we get an Array of actual words,
  109.             // we slice that Array and then get the
  110.             // last array-element from that array,
  111.             // using Array.prototype.pop():
  112.             var w = getActualWords(n).slice(0, d.nth).pop(),

  113.                 // here we get the index of that word
  114.                 // (note that this is naive, and relies
  115.                 // upon the word being unique as a
  116.                 // proof-of-concept; I plan to update later):
  117.                 i = n.textContent.indexOf(w);

  118.                 // we split the n textNode into
  119.                 // two separate textNodes, at
  120.                 // supplied index ('i + w.length');
  121.                 // n remains the shortened 'first'
  122.                 // textNode:
  123.                 n.splitText(i + w.length);

  124.             // navigating to the parentNode, and
  125.             // using insertBefore() to insert the
  126.             // new element ('elem') before the
  127.             // next-siblin of the n textNode:
  128.             n.parentNode.insertBefore(elem, n.nextSibling);

  129.             // doing exactly the same, but adding a
  130.             // newly-created textNode (of a space character)
  131.             // between the 'n' textNode (which by definition
  132.             // ends without a space) and newly-inserted
  133.             // element:
  134.             n.parentNode.insertBefore(document.createTextNode(' '), n.nextSibling);

  135.             // joining adjacent, but unconnected,
  136.             // textNodes (n and the newly-inserted
  137.             // space character) together, to become
  138.             // a single node:
  139.             n.parentNode.normalize();

  140.             // returning the newly-created element
  141.             // so that it can be modified if required
  142.             // or simply cached:
  143.             return elem;
  144.         }

  145.     }
  146. }


  147. // calling the function, specifying the
  148. // user-defined properties:
  149. insertElemAfterNthWord({
  150.     // after the tenth word:
  151.     'nth': 10,
  152.     // the element-type (a span):
  153.         'elemTag': 'span',

  154.     // setting the text of that new element:
  155.         'elemText': 'this is the newly-added text inside the newly-added element!',

  156.     // specifying the node into which the element
  157.     // should inserted:
  158.         'node': document.querySelector('div > div')

  159. // chaining the function, to use the Element.classList
  160. // API to add the 'newlyAdded' class to the
  161. // newly-created element:
  162. }).classList.add('newlyAdded');


References: