garble function contest

1.8k views Asked by At

Remember that away message on aim that said how:

Aoccdrnig to a rscheearch at Cmabrigde Uinervtisy, it deosn't mttaer in waht oredr the ltteers in a wrod are, the olny iprmoetnt tihng is taht the frist and lsat ltteer be at the rghit pclae. The rset can be a toatl mses and you can sitll raed it wouthit porbelm. Tihs is bcuseae the huamn mnid deos not raed ervey lteter by istlef, but the wrod as a wlohe.

Anyway I'm trying to make a function that would do that to an entire page. There are a few rules for this function.

  1. less then 4 characters leave alone.
  2. non-alphanumeric characters don't count as part of the word.
  3. hyphenated words are really two words
  4. words must get garbled if length >= 4 (can't be like the original)
  5. The first and last chars stay the same and only the middle chars get garbled (Thanks Hersheezy)
  6. the text should always be random and produce unique garbling on each run
  7. Pure javascript and iterates on all text nodes
  8. Shortest sweetest code wins.

Anyway it seems simple enough to implement, how's about starting a contest to see who could make the cleanest clearest code to accomplish this task. Feel free to borrow without recognition from my code (I def have)

If i missed anything add it in the comments. Anyway I worked on it very hackishly and here's me showing my less than par work

DEMO

var i, j, words, textNodes, punct = /[^a-zA-Z0-9]/;

Array.prototype.shuffle = function() {
    for (var i = 0; i < this.length; i++) {
        var j = i;
        while (j == i) {
            j = Math.floor(Math.random() * this.length);
        }
        var tmp = this[i];
        this[i] = this[j];
        this[j] = tmp;
    }
    return this;
};

String.prototype.shuffle = function() {
    return this.split('').shuffle().join('');
};

function transverse(element, array) {
    if (!array) array = [];
    if (element.nodeType === 3) {
        array.push(element);
    } else {
        for (var i = 0; i < element.childNodes.length; i++) {
            transverse(element.childNodes[i], array);
        }
    }
    return array;
}

function garble(str) {
    if (!str) return '';
    str = str.trim();
    if (/-/.test(str)) {
        str = str.split('-');
        for (var i = 0; i < str.length; i++) {
            str[i] = garble(str[i]);
        }
        return str.join('-')
    }
    if (punct.test(str.charAt(0))) {
        return str.charAt(0) + garble(str.slice(1));
    }
    if (punct.test(str.charAt(str.length - 1))) {
        return garble(str.slice(0, -1)) + str.charAt(str.length - 1);
    }
    if (str.length < 4) return str;
    if (str.length === 4) return str.charAt(0) + str.charAt(2) + str.charAt(1) + str.charAt(3)
    return str.charAt(0) + str.substr(1, str.length - 2).shuffle() +
        str.charAt(str.length - 1);
}


window.onload = function() {
    textNodes = transverse(document.documentElement);
    for (i = 0; i < textNodes.length; i++) {
        words = textNodes[i].data.split(' ');
        for (j = 0; j < words.length; j++) {
            words[j] = garble(words[j]);
        }
        textNodes[i].data = words.join(' ');
    }
};
4

There are 4 answers

4
Amjad Masad On BEST ANSWER

UPDATE( LATEST ): Don't think it can get any smaller.. DEMO
Latest compressed (332):

var e=document.body.getElementsByTagName('*'),j,i,l,x,t,b;for(i=0;e[i];i++)for(j=0;b=e[i].childNodes[j];j++)if(b.nodeType==3)b.data=b.data.replace(/\w{4,}/g,function(w){if(/(^.)(\1)+$/.test(x=w.substring(1,l=w.length-1)))return w;t=w;while(t==w)t=w[0]+x.split('').sort(function(){return 0.5-Math.random()}).join('')+w[l];return t}); 

code:

var e = document.body.getElementsByTagName('*'),
    j, i, l, x, t, b;
for (i = 0; e[i]; i++)
for (j = 0; b = e[i].childNodes[j]; j++)
if (b.nodeType == 3) b.data = b.data.replace(/\w{4,}/g, function(w) {
    if (/(^.)(\1)+$/.test(x = w.substring(1, l = w.length - 1))) return w;
    t = w;
    while (t == w)
    t = w[0] + x.split('').sort(function() {
        return 0.5 - Math.random();
    }).join('') + w[l];
    return t;
});

UPDATE even.. smaller..

Even smaller version
I dont know the minifier your using, but this must be at least (EDIT 108) bytes smaller.
compressed version (365 bytes):

var e=document.body.getElementsByTagName('*'),a=[],c,j,i,l,x,t,b;for(i=0;c=e[i];i++)for(j=0;b=c.childNodes[j];j++)if(b.nodeType==3){b.data=b.data.replace(/\b[a-z0-9]{4,}\b/gi,function(w){if(/(^.)(\1)+$/.test(x=w.substring(1,l=w.length-1)))return w;t=w;while(t==w)t=w[0]+x.split('').sort(function(){return Math.floor(Math.random()*2)?1:-1}).join('')+w[l];return t})}  

Code:

var e = document.body.getElementsByTagName('*'),
    a = [],
    c, j, i, l, x, t, b;
for (i = 0; c = e[i]; i++)
for (j = 0; b = c.childNodes[j]; j++)
if (b.nodeType == 3) {
    b.data = b.data.replace(/\b[a-z0-9]{4,}\b/gi, function(w) {
        if (/(^.)(\1)+$/.test(x = w.substring(1, l = w.length - 1))) return w;
        t = w;
        while (t == w)
        t = w[0] + x.split('').sort(function() {
            return Math.floor(Math.random() * 2) ? 1 : -1;
        }).join('') + w[l];
        return t;
    });
}

EDIT
NEW RULES DEMO
CODE:

var fn = function(e) {
    var ret = [],c;
    for (var i = 0; i < e.length; i++) {
        c = e[i].childNodes;
        for (var j = 0; j < c.length; j++)
            if (c[j].nodeType === 3) ret.push(c[j]);
    }
    return ret;
};
var es = fn(document.body.getElementsByTagName('*'));
for (var i = 0; i < es.length; i++) {
    var e = es[i],len,x;
    e.data = e.data.replace(/\b[a-z0-9]{4,}\b/gi, function(w) {
        if (/(^.)(\1)+$/.test(x = w.substring(1, len = w.length - 1))) return w;
        var tmp = w;
        while (tmp === w) {
            tmp = w[0] + x.split('').sort(function() {
                return Math.floor(Math.random() * 2) ? 1 : -1;
            }).join('') + w[len];
        }
        return tmp;
    });
}

This should respect all the rules, and keep format and punctuation. DEMO

//select all nodes in document and perform map on it to filter out
//non text node types, then each one of those elements is processed.
$('*').contents().map(function(i, elem) {
    if (elem.nodeType !== 3) return null;
    else return elem;
}).each(function(i, elem) {
 //call strip funciton defined down to get an object, with a word array, and
 //charecters which was stripped along with there index in the orginal string
    var str1 = '',
        tmp = strip(elem.data),
        words = tmp.words,
        sentence;
    // shuffle all words
    words = $.map(words, function(x, i) {
        return shuffle(x);
    });
    //construct raw sentence (non alphanumeric charecters)
    sentence = words.join('');
    //reinsert spaces and punctiouation 
    $.each(tmp.chars, function(i, elem) {
        sentence = sentence.substring(0, elem.index) + elem.char + sentence.substring(elem.index - 1 + elem.char.length);
    });
    //set the element text
    elem.data = sentence;
});

//shuffle funciton takes a word and shuffle the charecters between the last and the firt
function shuffle(txt) {
    //if the word is smaller than 4 charecters or it has repeated charecters in
    //its middle (i.e. loop, 'oo' cannot be shuffled!) then return it;
    if (txt.length < 4 || /(^.)(\1)+$/.test(txt.substring(1, txt.length - 1)))
        return txt;
    var str = txt.split(''),
        ret = [],
        rand, x = 0,
        tmp = txt;
    //while the txt hasn't changed in the first randomization cycle then repeated
    while (txt === tmp) {
        ret = [];
        $.each(str, function(i, c) {
            if (i === str.length - 1 || i === 0) {
                ret[i] = c;
                return;
            }
            while (true) {
                rand = Math.floor(Math.random() * (str.length - 2) + 1);
                if (!ret[rand]) {
                    ret[rand] = c;
                    break;
                }
            }
        });
        tmp = ret.join('');
    }
    return ret.join('');
}

function strip(txt) {
    var punc = /[^A-Za-z0-9]/g,
        res, nonAlphaNum = [],
        arr;
    //punc regex is all non-alphanumeric charecters which will act on the string
    //to point out unwanted charecters and store them in an array along with
    //their index
    while ((res = punc.exec(txt)) != null) {
        nonAlphaNum.push({
            index: res.index,
            char: res[0]
        });
    }
    //split into words
    arr = txt.split(/\s/);
    //remove punctiuation and other unwanted chars
    arr = $.map(arr, function(x, i) {
        return x.replace(punc, '');
    });
    return {
        words: arr,  //words array
        chars: nonAlphaNum //array of stripped charecter objects (chars, index in orginal)
    };
} 

btw nice choice of the article, WWiWieikikb!!

17
Brad Christie On

Updated

So I couldn't help but play around a bit with this thing and see what other ways I could manipulate the document with as little code as possible. Suffice it to say that it can be shortened to work in an either/or scenario, but I like to make things with options for the user to play with.

Having said that, here are some variations on the above and benefits/disappointments:


Official Submission (473bytes)

Minified (473bytes) 1

function t(e){var r=[],w;for(w=0;w<e.length;w++){if(e[w].nodeType===3)r.push(e[w]);else if(e[w].childNodes)r=r.concat(t(e[w].childNodes));}return r;}var e,x=t(document.body.childNodes),y,z;for(y=0;y<x.length;y++){x[y].data=x[y].data.replace(/\b[a-z]{4,}\b/ig,function(w){if(w.length==4&&(/^.([a-z])\1./i).test(w))return w;e=w;while(e==w){z=w.split('');e=z[0]+(z.slice(1,z.length-1).sort(function(a,b){return(Math.random()*2)>1?1:-1;}).join(''))+z[z.length-1];}return e;});}

Un-minified version: (479bytes) 1

function t(e){
  var r=[],w;
  for(w=0;w<e.length;w++){
    if(e[w].nodeType===3)r.push(e[w]);
    else if(e[w].childNodes)r=r.concat(t(e[w].childNodes));
  }
  return r;
}
var x=t(document.body.childNodes);
for(var y=0;y<x.length;y++){
  x[y].data=x[y].data.replace(/\b[a-z]{4,}\b/ig,function(w){
    if(w.length==4&&(/^.([a-z])\1./i).test(w))
      return w;
    var e=w;
    while (e==w){
      var x=w.split('');
      e=x[0]+(x.slice(1,x.length-1).sort(function(a,b){
        return(Math.random()*2)>1?1:-1;
      }).join(''))+x[x.length-1];
    }
    return e;
  });
}
  • No use of jQuery ("pure javascript")
  • Add <script src="garble.js"></script> just above </body> or wrap the code in an onload event.

1 re-placement of var declarations makes it shorter, see 479bytes vs 473 byes)


Additional Versions

Basic (demo)

// jQuery Garble
// "Basic" version
//
// Requirements:
// 1. Find all words 4+ letters long (exclude hyphens, punctuation or numbers from
//    the classification)
// 2. The words being garbled must follow:
//    a. They can not remain the same as the previous state
//    b. The first and last character must remain in-tact
// 3. The garbling must be random and produce a new result each iteration.
//
// Usage:
// $(selector).garble(options);
//
(function($){
    $.fn.extend({
        garble: function(options){
            // basic options
            var o = $.extend({
                flagChanges: false,
                changeClass: 'modified'
            },options);
            // iterate over elements
            return this.each(function(i,e){
                var txt = $(e).text();
                // locate words with 4+ letters
                $(e).html(txt.replace(/\b[a-z]{4,}\b/ig,function(w){
                    var e = w;
                    // make sure we get an altered word back
                    while (e==w){
                        var x = w.split('');
                        e = x[0]+(x.slice(1,x.length-1).sort(function(y,z){
                            return (Math.random()*2)>1?1:-1; // randomize
                        }).join(''))+x[x.length-1];
                    }
                    return (o.flagChanges?'<span class="'+o.changeClass+'">'+e+'</span>':e);
                }));
            });
        }
    });
})(jQuery);

Pros

  1. Very slim and trim
  2. Options that allow you to modify the modified word (wraps each change in a span with the default "modified" class, or a class of your choosing).

Cons

  1. Won't work with nested elements (this means you have to select the lowest possible element in the DOM tree. So, if you're just going to be doing paragraphs with no hyperlinks or spans within, this is your winner)
  2. If elements that have children are used in the selector, their html formatting (such as a linkin a paragraph) will be stripped.

Slim and Trim (demo)

$(function(){                                                              // on load
  $('*','body').contents().map(function(i,e){                              // grab all elements,
    return e.nodeType !== 3 ? null : e;                                    // then filter by textual elements
  }).each(function(i,e){                                                   // amd iterate through them.
    e.data = e.data.replace(/\b[a-z]{4,}\b/ig, function(w) {               // go through and find 4+ letters words
      if (w.length==4&&w.substring(1,2)==w.substring(2,3))                 // (avoid infinite loops on words that
        return w;                                                          // can't be changed [e.g. look])
      var e = w;                                                           // Store the original word for comparison, but
      while (e==w){                                                        // keep scrambling until we have a new word.
        var x = w.split('');                                               // (do so by breaking out middle letters in to array,
        e = x[0] + (x.slice(1, x.length - 1).sort(function(a,b){           // then sort those middle letters
          return (Math.random() * 2) > 1 ? 1 : -1;                         // based on a random number)
        }).join('')) + x[x.length - 1];                                    // Now, rejoin it all back together
      }
      return e;                                                            // and finally return the modified result.
    });
  });
});

Fully Featured (demo)

// jQuery Garble
// "Feature Rich" version
//
// Requirements:
// 1. Find all words 4+ letters long (exclude hyphens, punctuation or numbers from
//    the classification)
// 2. The words being garbled must follow:
//    a. They can not remain the same as the previous state
//    b. The first and last character must remain in-tact
// 3. The garbling must be random and produce a new result each iteration.
//
// Usage:
// $(selector).garble(options);
//
(function($) {
    $.fn.extend({
        garble: function(options) {
            var o = $.extend({}, $.fn.garble.defaults, options);

            // takes in a string and performs the necessary manipulation(s) on it. Use regex
            // to only collect words greater than or equal to 4 characters long, and consider
            // punctuation not part of the word.
            var garbleStr = function(s,t){
                return s.replace(/\b[a-z]{4,}\b/ig, function(w) {
                    var e = o.algorithm(w);

                    // if we're not performing a low-level parse and they want the changes styled,
                    // return a span with either the detault class or their custom class
                    if (t && !o.lowLevel && o.highLevel.flagChanges)
                        return '<span class="'+o.highLevel.changeClass+'">'+e+'</span>';

                    // just return the new word
                    return e;
                });
            };

            // Very high-level process.
            // Will only change the lowest node's text (so a paragraph
            // with links, only the links will be altered)
            var highLevel = function(i, e) {
                // we're not at the bottom element, keep going
                if ($(e).children().length>0){
                    return $(e).children().each(highLevel);
                }

                var t = $(e).text();
                $(e).html(garbleStr(t,e.tagName!=='TEXTAREA'));
            };
            // Low level process
            // Goes down to each individual element and changes it
            var lowLevel = function(i, e) {
                var d = e.data;
                e.data = garbleStr(d);
            };

            // depending their selection, execute either or
            if (o.lowLevel){
                return this.find('*').contents().map(function(i, e) {
                    return (e.nodeType !== 3 ? null : e);
                }).each(lowLevel);
            }else{
                return this.contents().each(highLevel);
            }
        },
    });

    // Defaults
    $.fn.garble.defaults = {
        // Perform low-level modification? (Modifies all words it finds,
        // not just the one farthests down the tree)
        lowLevel: false,

        // when lowLevel is set to false, these options are available:
        highLevel: {
            // wrap changes in a <span>
            flagChanges: false,

            // the class being applied to the above <span>
            changeClass: 'modified'
        },

        // function used to garble the text. This will be passed each word
        // individually and should return the new word's value.
        algorithm: function(w) {
            // if w = "hello":
            // 1. Make an array out of the letters.
            // 2. keep the first and last in-tact, but use .slice() to extract the middle
            // 3. Perform the specified algorithm on the middle characters
            // 4. return result
            var e = w;
            while (e==w){ // secure it gets changed
                var x = w.split('');
                e = x[0] + (x.slice(1, x.length - 1).sort(function(a,b){
                    return (Math.random() * 2) > 1 ? 1 : -1;
                }).join('')) + x[x.length - 1];
            }
            return e;
        }
    };
})(jQuery);

Pros

  1. Flexible. This will work in just about every scenario, albeit a quick-to process bottom-of-the-food-chain element or calling it on the entire <body>--this can handle it.
  2. Very customizable:
    1. Can specify to perform low level (every element) changes, or highLevel (only elements with no children)
    2. Can specify if you want to show the changes, and what class to apply (changes are wrapped in a span with the specified class name)
    3. Can specify a function to use for scrambling (maybe you want to just reverse the words or use a better method)

Cons

  1. Little more overhead, though it is still fast.
  2. Maybe too many options or just too bloated.
  3. I'm sure someone will find more cons
0
Jack Senechal On

Here's my take on the function. See the demo here.

  • It uses the excellent jquery.ba-replacetext plugin to handle finding only text nodes
  • Handles arbitrary length words where all inner characters are equal
  • Easy to understand with descriptive names

Full version (1187 bytes):

  /* Called on document.ready */
  $(function () {
    $("body *").replaceText(/\b([A-z]{4,})\b/g, scramble_inner );
  });
  /* Scramble the inner characters of a word */
  function scramble_inner(word) {
    return word[0] 
      + force_shuffle(word.slice(1, word.length - 1))
      + word[word.length - 1];
  }
  /* Randomize characters in the string, but check to make sure
   * they're different from the original. Handle's the special
   * case where all inner characters are equal, for instance "moooo".
   */
  function force_shuffle(str) {
    if (all_chars_same(str)) return str;
    var result = str;
    while (str === result) {
      result = str.split('').sort(function() {
        return Math.floor(Math.random() * 2) ? 1 : -1;
      }).join('');
    }
    return result;
  }
  /* Check whether all characters in the string are equal, eg "ooo" */
  function all_chars_same(str) {
    for (i = 0; i < str.length; i++) {
      if (str[i] !== str[0]) {
        return false;
      }
    }
    return true;
  }

Minified version (348 bytes):

$(function(){$("body *").replaceText(/\b([A-z]{4,})\b/g,a)});function a(w){return w[0]+b(w.slice(1,w.length-1))+w[w.length-1]}function b(s){if(c(s))return s;var r=s;while(s===r){r=s.split('').sort(function(){return Math.floor(Math.random()*2)?1:-1}).join('')}return r}function c(s){for(i=0;i<s.length;i++){if(s[i]!==s[0]){return false}}return true}
0
Neil On

Note: I ran this through JSLint, which may have been a bad idea. Anyway, the demo is here.

function shuffle(letters) {
    var i = letters.length - 2;
    while (i > 1) {
        var pos = Math.floor(Math.random() * i) + 1;
        var tmp = letters[i];
        letters[i] = letters[pos];
        letters[pos] = tmp;
        i--;
    }
}

function scramble(word) {
    if (word.slice(1, -2) == word.slice(2, -1)) {
        return word;
    }
    var letters = word.split('');
    var result = word;
    while (result == word) {
        shuffle(letters);
        result = letters.join('');
    }
    return result;
}

function process(node) {
    var data = node.data;
    if (/[a-z]{4}/i.test(data)) {
        node.data = data.replace(/[a-z]{4,}/gi, scramble);
    }
}

function traverse(element) {
    var node = element.firstChild;
    while (node) {
        if (node.nodeType == Node.ELEMENT_NODE) {
            traverse(node);
        } else if (node.nodeType == Node.TEXT_NODE) {
            process(node);
        }
        node = node.nextSibling;
    }
}

function garble() {
    traverse(document.body);
}

window.onload = garble;