<!DOCTYPE html>
<html>

  <head>
    <link rel="stylesheet" href="style.css">
  </head>

  <body>
    
    <h3>basic usage</h3>
    
    <pre>var parser = new Drycleaner(someString); 
var str = parser.escape().str;</pre>
    
    <textarea id="basic-edit"></textarea> <br />
    result: <pre id="basic-disp"></pre>
    
    <h3>with exceptions</h3>
    
    <pre>var parser = new Drycleaner(someString); 
var str = parser.escape({except: ["&amp;"]}).str;</pre>
    
    <textarea id="except-edit"></textarea> <br />
    result: <pre id="except-disp"></pre>
    
    <h3>cache initial argument</h3>
    
    <pre>var parser = new Drycleaner("&lt;b&gt;initial string&lt;/b&gt;"); 
var html = parser.escape({result: "res"})
var cache = html.str;
var processed = html.res;</pre>
    
    <textarea id="result-edit"><b>initial string</b></textarea> <br />
    cache: <pre id="result-disp-init"></pre>
    result: <pre id="result-disp"></pre>
    
    <h3>enable white space parsing</h3>
    
    <pre>var parser = new Drycleaner(someString); 
var str = parser.escape({nbsp: true}).str;</pre>
    
    <textarea id="space-edit"></textarea> <br />
    result: <pre id="space-disp"></pre> 
    
    <h3>wrap result in html tag</h3>
    
    <pre>var parser = new Drycleaner("&lt;b&gt;hello world&lt;/b&gt;"); 
var str = parser.escape()
                .wrap({ el: "pre", result: "pre" })
                .escape({ workOn: "pre", result: "post" })
                .post
                .outerHTML;</pre>
    
    stringified result: <span id="wrap-disp"></span> <br />
    rendered result: <span id="wrap-ren"></span>
    
    <script src="drycleaner.js"></script>
    <script src="script.js"></script>
  </body>

</html>

// Code goes here

(function(obj) {
  
  function basic() {
    var parser = new Drycleaner(this.value);
    var str = parser.escape().str;
    obj.basic.disp.textContent = str;
  }
  
  function except() {
    var parser = new Drycleaner(this.value);
    var str = parser.escape({except: ["&"]}).str;
    obj.except.disp.textContent = str;
  }
  
  function result() {
    var parser = new Drycleaner(this.value);
    var html = parser.escape({to: "res"});
    if (!obj.result.initState) {
      obj.result.initState = true;
      obj.result.init.textContent = html.str;
    }
    obj.result.disp.textContent = html.res;
  }
  
  function space() {
    var parser = new Drycleaner(this.value);
    var str = parser.escape({nbsp: true}).str;
    obj.space.disp.textContent = str;
  }
  
  function wrap() {
    var parser = new Drycleaner("<b>hello world</b>");
    var str = parser.escape()
                    .wrap({ el: "pre", to: "pre" })
                    .escape({ from: "pre", to: "post"  })
    obj.wrap.ren.appendChild(str.pre)
    obj.wrap.disp.textContent = str.pre.outerHTML
  }
  
  obj.basic.edit.addEventListener("keyup", basic, false);
  obj.except.edit.addEventListener("keyup", except, false);
  obj.result.edit.addEventListener("keyup", result, false);
  obj.space.edit.addEventListener("keyup", space, false);
  wrap();
  
})({
  basic: {
    edit: document.getElementById("basic-edit"),
    disp: document.getElementById("basic-disp")
  },
  except: {
    edit: document.getElementById("except-edit"),
    disp: document.getElementById("except-disp")
  },
  result: {
    edit: document.getElementById("result-edit"),
    init: document.getElementById("result-disp-init"),
    disp: document.getElementById("result-disp")
  },
  space: {
    edit: document.getElementById("space-edit"),
    disp: document.getElementById("space-disp")
  },
  wrap: {
    disp: document.getElementById("wrap-disp"),
    ren: document.getElementById("wrap-ren")
  }
});
/* Styles go here */

pre {
  margin:0;
  padding:0;
  background-color:#E6E6E6;
  width:100%;
}

textarea {
  margin-top:10px;
  width:99%;
}

.edit {
  background-color:#D8D8D8;
  height:50%;
  width:100%;
}

#wrap-disp {
  font-family: "courier";
  font-size: 11pt;
}
demo for drycleaner, a tiny library for manipulating html entity strings

github.com/daniellizik/drycleaner
/**
* List of defaults. It's easier to pass a "ghost" version
* if a user-specified options object is not passed.
*
* @nbsp {boolean}: if true, all nbsp will be replaced as &nbsp; default is false
* @except {array}: array of characters/strings that will not be transformed
* @from {string}: name of object property that will be worked on
* @to {string}: name of object property that is to be assigned to product of conversion
* @wrap {object}: wraps initializing string in html element, takes el and style properties
* @wrap.el {string}: name of html element you want to wrap string in
* @wrap.style {object}: inline style of the html element
*
**/

var Drycleaner = (function() {
  "use strict";

  var matcher = {
    space: /^\s*$/g,
    dom: /\[object HTML\w+Element\]/,
    str: "[object String]",
    obj: "[object Object]",
    arr: "[object Array]",
    rx: "[object RegExp]"
  };

  var HTML_ENTITIES = [
    { str: "&amp;", char: "&", charRx: /&(?!.*\;)/g },
    { str: "&quot;", char: '"', charRx: /\"/g },
    { str: "&lt;", char: "<", charRx: /\</g },
    { str: "&gt;", char: ">", charRx: /\>/g },
    { str: "&#39;", char: "'", charRx: /\'/g },
    { str: "&nbsp;", char: " ", charRx: /\s{1}/g }
  ];

  var err = {
    empty: "Cannot pass an empty string.",
    notString: "Can only pass a string into drycleaner.",
    undeclared: "This property has not been declared.",
    notArray: "The except property must be an array.",
    rx: "Invalid regular expression.",
    notDefined: "Trying to pass undefined variable.",
    shouldBeBoolean: "The parameter must be a boolean.",
    unspecifiedElement: "Must specifiy an html element in the el property.",
    wrongProps: "This option property is not supported.",
    reserved: "You cannot set 'from' or 'to' to the name of a publicly available method."
  };

  var ghostDefaults = {
    global: {
      from: "str",
      to: false
    },
    convert: {
      nbsp: false,
      except: [],
    },
    wrap: {
      el: "div",
      style: false,
    }
  };

  var conf = [
    {
      public: "escape",
      fn: configurator({ defaults: "convert", from: "charRx", to: "str", exceptId: "char", fn: convertPrimer })
    },
    {
      public: "unescape",
      fn: configurator({ defaults: "convert", from: "str", to: "char", exceptId: "str", fn: convertPrimer })
    },
    {
      public: "wrap",
      fn: configurator({ defaults: "wrap", fn: wrap })
    }
  ];

  function init(input) {
    //cannot be "" or only whitespace
    if (matcher.space.test(input)) throw new Error(errMsg.empty);
    return new Drycleaner(input);
  }

  function Drycleaner(str) {
    this.str = matcher.dom.test(getType(str)) === true ? str.outerHTML : str;
  }

  //first take config options 
  function configurator(config) {
    //this is the returned function that will be publicly available
    return function optionHandler(userOptions) {
      //userOptions must an object if something is passed
      if (userOptions && getType(userOptions) !== matcher.obj) throw new Error(errMsg.notObject);
      //test for invalid options; send user options, global defaults and method-specific options send from configurator arguments
      if (getOptionKeys(userOptions, ghostDefaults.global, ghostDefaults[config.defaults]) === false) throw new Error(errMsg.wrongProps);
      //merge ghost defaults and user options
      var options = !userOptions ? merge({}, ghostDefaults.global, ghostDefaults[config.defaults]) : merge(userOptions, ghostDefaults.global, ghostDefaults[config.defaults]);
      //check if user set to or from as one of the public methods
      preventOverwrite(options);
      //set the result property
      var to = options.to === false ? "str" : options.to;
      this[to] = config.fn.call(this, config, options);
      return this;
    }
  }

  function convertPrimer(config, options) {
    //except option must be array (if not specified, it comes from ghost defaults, which by default is an array)
    if (getType(options.except) !== matcher.arr) throw new Error(errMsg.notArray);
    var tmpStr = setFrom.call(this, options); 
    return loopCharacters.call(this, tmpStr, config, options);
  }

  function setFrom(options) {
    if (options.from && !this[options.from]) throw new Error(errMsg.undeclared);
    if (options.from === "str") return this.str;
    if (options.from && this[options.from]) {
      if (matcher.dom.test(getType(this[options.from]))) return this[options.from].outerHTML;
      else return this[options.from];
    }
    
  }

  function loopCharacters(tmpStr, config, options) {
    var rxPre;
    for (var i = 0; i < HTML_ENTITIES.length; i++) {
      var obj = HTML_ENTITIES[i];       
      //matches with exception array
      var index = options.except.indexOf(obj[config.exceptId]);
      //if the current html entity is being excepted via user options replace the same thing (nothing is being converted)
      var rxPre = index === -1 ? makeRegex(obj[config.from]) : obj[config.to];
      var rxPost = (options.nbsp === true && obj.char === " ") ? rxPre : obj[config.to];
      var rxFinal = (options.nbsp === false && obj.char === " ") ? obj[config.to] : rxPre;
      tmpStr = convert(tmpStr, rxFinal, obj[config.to]);
    }
    return tmpStr;
  }

  function convert(str, regex, replacer) {
    if (regex === replacer) return str;
    var tmp = str.replace(regex, replacer);
    return tmp;
  }

  function wrap(config, options) {
    if (options.el === false) return this.str;
    if (options && !options.el) throw new Error(errMsg.unspecifiedElement);
    var wrap = document.createElement(options.el);
    if (options.style !== false) {
      for (var p in options.style) wrap.style[p] = options.style[p];
    }
    //using outerHTML or innerHTML will escape html characters again
    wrap.textContent = this[options.from];
    return wrap;
  }

  function makeRegex(input) {
    if (getType(input) === matcher.rx) return input;
    if (getType(input) === matcher.str) return new RegExp(input, "g");
  }

  function getOptionKeys(options, globals, defaults) {
    //if no options are supplied that means they couldn't have supplied any wrong options
    if (!options) return true;
    var userKeys = getKeys(options);
    var defaultKeys = getKeys(defaults).concat(getKeys(globals));
    for (var i = 0; i < userKeys.length; i++) {
      if (defaultKeys.indexOf(userKeys[i]) === -1) return false;
    }
    return true;
  }

  function getKeys(obj) {
    var keys = [];
    for (var p in obj) {
      if (keys.indexOf(p) === -1) keys.push(p);
    }
    return keys;
  } 

  function preventOverwrite(obj) {
    var publicMethods = conf.map(function(o){ return o.public; });
    if (publicMethods.indexOf(obj.to) > -1 || publicMethods.indexOf(obj.from) > -1) throw new Error(err.reserved);
  }

  function merge() {
    var args = Array.prototype.slice.call(arguments);
    var base = args.slice(0, 1)[0];
    var objs = args.slice(1);
    for (var i = 0; i < objs.length; i++) {
      for (var p in objs[i]) {
        if (!base[p]) {
          base[p] = objs[i][p];
        }
      }
    }
    return base;
  } 

  function getType(str) {
    return Object.prototype.toString.call(str);
  }

  //set public methods
  conf.forEach(function(obj){ Drycleaner.prototype[obj.public] = obj.fn; });

  return init;
  
})();