/*=:project
		parseSelector 2.0
		
	=:description
		Provides an extensible way of parsing CSS selectors against a DOM in 
		JavaScript.

  =:file
  	Copyright: 2006 Mark Wubben.
  	Author: Mark Wubben, <http://novemberborn.net/>
   		
	=:license
		This software is licensed and provided under the CC-GNU LGPL. 
		See <http://creativecommons.org/licenses/LGPL/2.1/>
		
	=:support
	  parseSelector supports the following user agents:
	    * Internet Explorer 6 and above
	    * Firefox 1.0 and above, and equivalent Gecko engine versions
	    * Safari 2.0 and above
	    * Opera 8.0 and above
	    * Konqueror 3.5.5 and above
	  It might work in other browsers and versions, but there are no guarantees. There is
	  no verification made when parseSelector is run to ascertain the browser is supported.
	
	=:notes
		The parsing of CSS selectors as streams has been based on Dean Edwards
		excellent work with cssQuery. See <http://dean.edwards.name/my/cssQuery/>
		for more info.
*/

var parseSelector = (function() {
	var SEPERATOR       = /\s*,\s*/
	var WHITESPACE      = /\s*([\s>+~(),]|^|$)\s*/g;
	var IMPLIED_ALL     = /([\s>+~,]|[^(]\+|^)([#.:@])/g;
	var STANDARD_SELECT = /^[^\s>+~]/;
	var STREAM          = /[\s#.:>+~()@]|[^\s#.:>+~()@]+/g;
	
	function parseSelector(selector, node) {
		node = node || document.documentElement;
		var argSelectors = selector.split(SEPERATOR), result = [];

		for(var i = 0; i < argSelectors.length; i++) {
			var nodes = [node], stream = toStream(argSelectors[i]);
			for(var j = 0; j < stream.length;) {
				var token = stream[j++], filter = stream[j++], args = '';
				if(stream[j] == '(') {
					while(stream[j++] != ')' && j < stream.length) args += stream[j];
					args = args.slice(0, -1);
				}
				nodes = select(nodes, token, filter, args);
			}
			result = result.concat(nodes);
		}
		
		return result;
	}

	function toStream(selector) {
		var stream = selector.replace(WHITESPACE, '$1').replace(IMPLIED_ALL, '$1*$2');
		if(STANDARD_SELECT.test(stream)) stream = ' ' + stream;
    return stream.match(STREAM) || [];
	}
	
	function select(nodes, token, filter, args) {
		return (selectors[token]) ? selectors[token](nodes, filter, args) : [];
	}
	
	var util = {
		toArray: function(enumerable) {
			var a = [];
			for(var i = 0; i < enumerable.length; i++) a.push(enumerable[i]);
			return a;
		}
	};
	
	var dom = {
		isTag: function(node, tag) {
			return (tag == '*') || (tag.toLowerCase() == node.nodeName.toLowerCase());
		},
	
		previousSiblingElement: function(node) {
			do node = node.previousSibling; while(node && node.nodeType != 1);
			return node;
		},
	
		nextSiblingElement: function(node) {
			do node = node.nextSibling; while(node && node.nodeType != 1);
			return node;
		},
	
		hasClass: function(name, node) {
			return (node.className || '').match('(^|\\s)'+name+'(\\s|$)');
		},
	
		getByTag: function(tag, node) {
			return node.getElementsByTagName(tag);
		}
	};

	var selectors = {
		'#': function(nodes, filter) {
			for(var i = 0; i < nodes.length; i++) {
				if(nodes[i].getAttribute('id') == filter) return [nodes[i]];
			}
			return [];
		},

		' ': function(nodes, filter) {
			var result = [];
			for(var i = 0; i < nodes.length; i++) {
				result = result.concat(util.toArray(dom.getByTag(filter, nodes[i])));
			}
			return result;
		},
		
		'>': function(nodes, filter) {
			var result = [];
			for(var i = 0, node; i < nodes.length; i++) {
				node = nodes[i];
				for(var j = 0, child; j < node.childNodes.length; j++) {
					child = node.childNodes[j];
					if(child.nodeType == 1 && dom.isTag(child, filter)) result.push(child);
				}
			}
			return result;
		},

		'.': function(nodes, filter) {
			var result = [];
			for(var i = 0, node; i < nodes.length; i++) {
				node = nodes[i];
				if(dom.hasClass([filter], node)) result.push(node);
			}
			return result;
		}, 
				
		':': function(nodes, filter, args) {
			return (pseudoClasses[filter]) ? pseudoClasses[filter](nodes, args) : [];
		}
		
	};

	parseSelector.selectors			= selectors;
	parseSelector.pseudoClasses = {};
	parseSelector.util 				  = util;
	parseSelector.dom 				  = dom;

	return parseSelector;
})();

/*=:project
    scalable Inman Flash Replacement (sIFR) version 3, beta 1

  =:file
    Copyright: 2006 Mark Wubben.
    Author: Mark Wubben, <http://novemberborn.net/>

  =:history
    * IFR: Shaun Inman
    * sIFR 1: Mike Davidson, Shaun Inman and Tomas Jogin
    * sIFR 2: Mike Davidson, Shaun Inman, Tomas Jogin and Mark Wubben

  =:license
    This software is licensed and provided under the CC-GNU LGPL.
    See <http://creativecommons.org/licenses/LGPL/2.1/>    
*/

var sIFR = new function() {
  //=:private Constant reference to the Singleton instance
  var SIFR = this;

  var CSS_HASFLASH      = 'sIFR-active';
  var CSS_REPLACED      = 'sIFR-replaced';
  var CSS_FLASH         = 'sIFR-flash';
  var CSS_IGNORE        = 'sIFR-ignore';
  var CSS_ALTERNATE     = 'sIFR-alternate';
  var CSS_CLASS         = 'sIFR-class';
  var CSS_LAYOUT        = 'sIFR-layout';
  var XHTML_NS          = 'http://www.w3.org/1999/xhtml';
  var MIN_FONT_SIZE     = 6;
  var MAX_FONT_SIZE     = 126;
  var MIN_FLASH_VERSION = 8;
  var PREFETCH_COOKIE   = 'SIFR-PREFETCHED';
  //= Whitespace string each whitespace character is replaced with, as per `preserveSingleWhitespace`.
  var SINGLE_WHITESPACE = ' ';

  //= `true` if sIFR has been activated, `false` otherwise.
  this.isActive = false;
  //= Set this to `false` to disable sIFR even after it has been activated.
  this.isEnabled = true;
  //= Set to `true` to make sIFR set the CSS_HASFLASH class during activation, `false` to set it during replacement.
  this.hideElements = true;
  //= Controls the replacement of elements which are not visible on the page.
  this.replaceNonDisplayed = false;
  //= If set to `true`, sIFR will replace each individual whitespace character with the `SINGLE_WHITESPACE`
  //  string. Otherwise all consecutive whitespace characters will be replaced by the first character 
  //  in the sequence.
  this.preserveSingleWhitespace = false;
  //= Flash can wrap long non-broken text. To do this, however, it needs the correct width of 
  //  the text. In IE the width will be incorrect if the text is too long (and needs to be 
  //  wrapped by Flash). This property enables the workaround.
  this.fixWrap = true;
  //= Set to `false` to disable automatic initialization of sIFR. This shifts responsibility
  //  of initializing sIFR to the implementor.
  this.registerEvents = true;
  //= To prevent needless pre-fetching, a session cookie can be set. Set this property to `false`
  //  to stop the cookie from being set. *Warning:* this may cause needless pre-fetching!
  this.setPrefetchCookie = true;
  //= Default path for which the cookie is set. If your site has different areas which require
  //  different Flash movies, you should configure the `cookiePath` for each area.
  this.cookiePath = '/';
  //= Contains the domain names for which sIFR will activate. When set properly this will prevent
  //  the sIFR implementation to run when the site is loaded from other domains, such as the
  //  Google Translator service.
  this.domains = [];
  //= Allow sIFR to run when loaded from the `localhost` domain. You don't have to add `localhost`
  //  to the `domains`.
  this.fromLocal = true;
  //= In Gecko, preceding the sIFR element with a floated element, inside a fixed-width wrapper,
  //  gives wrong dimensions. Set this property to `true` to force the temporary clearing of the
  //  sIFR element, which works around the problem.
  this.forceClear = false;
  //= Force the Flash movie to take it's calculated width, instead of 100%. This may solve
  //  issues with the Flash movie being clipped or hidden. It may also have negative
  //  side effects.
  this.forceWidth = false;
  //= When `true` the dimensions of the Flash movie will fit exactly around the width and height
  //  of the text in the movie. *Note:* this is as reported by Flash, so you might need to 
  //  manually tune the dimensions.
  this.fitExactly = false;
  //= Let JavaScript force CSS text transformation. In the future, if Flash supports
  //  this natively, you might  want to change this property depending on the Flash version.
  this.forceTextTransform = true;
  //= By default, sIFR will replace elements before the page has fully loaded. It is possible
  //  that, for example, images affect the layout of the page and cause sIFR to improperly render.
  //  In that case, set the property to `false` so the elements will be replaced when the page
  //  is fully loaded. This property only has effect in Internet Explorer and Firefox. 
  //  If sIFR is pre-fetching movies for IE, DomContentLoaded won't be used.

  this.debugMode = false;
  //= True if the `CSS_HASFLASH` class is set. This can be either on the body element and the
  //  document element.
  this.hasFlashClassSet = false;
  
  var elementCount = 0; // The number of replaced elements.
  var hasPrefetched = false, isInitialized = false;

  var dom = new function() {
    this.getBody = function() {
      var nodes = document.getElementsByTagName('body');
      if(nodes.length == 1) return nodes[0];
      return null;
    };

    this.addClass = function(name, node) {
      if(node) node.className = ((node.className || '') == '' ? '' : node.className + ' ') + name;
    };
    
    this.removeClass = function(name, node) {
      if(node) node.className = node.className.replace(new RegExp('(^|\\s)' + name + '(\\s|$)'), '').replace(/^\s+|(\s)\s+/g, '$1');
    }

    this.hasClass = function(name, node) {
      return new RegExp('(^|\\s)' + name + '(\\s|$)').test(node.className);
    };

    this.create = function(name) {
      if(document.createElementNS) return document.createElementNS(XHTML_NS, name);
      return document.createElement(name);
    }
    
    this.setInnerHtml = function(node, html) {
      if(ua.innerHtmlSupport) node.innerHTML = html;
      else if(ua.xhtmlSupport){
        html = ['<root xmlns="', XHTML_NS, '">', html, '</root>'].join('');
        var xml = (new DOMParser()).parseFromString(html, 'text/xml');
        xml = document.importNode(xml.documentElement, true);
        while(node.firstChild) node.removeChild(node.firstChild);
        while(xml.firstChild)  node.appendChild(xml.firstChild);
      }
    };
    
    this.getComputedStyle = function(node, property) {
      var result;
      if(document.defaultView && document.defaultView.getComputedStyle) {
        result = document.defaultView.getComputedStyle(node, null)[property];
      } else if(node.currentStyle) result = node.currentStyle[property];
      return result || ''; // Ensuring a string.
    }

    this.getStyleAsInt = function(node, property, requirePx) {
      var value = this.getComputedStyle(node, property);
      if(requirePx && !/px$/.test(value)) return 0;
      
      value = parseInt(value);
      return isNaN(value) ? 0 : value;
    }

    this.getZoom = function() {
      return hacks.zoom.getLatest();
    }
  };
  this.dom = dom;

  var ua = new function() {
    var ua                = navigator.userAgent.toLowerCase();
    var product           = (navigator.product || '').toLowerCase();

    this.macintosh        = ua.indexOf('mac') > -1;
    this.windows          = ua.indexOf('windows') > -1;
    this.quicktime        = false;

    this.opera            = ua.indexOf('opera') > -1;
    this.konqueror        = product.indexOf('konqueror') > -1;
    this.ie               = false/*@cc_on || true @*/;
    this.ieSupported      = this.ie && !/ppc|smartphone|iemobile|msie\s5\.5/.test(ua)/*@cc_on && @_jscript_version >= 5.5 @*/
    this.ieWin            = this.ie && this.windows/*@cc_on && @_jscript_version >= 5.1 @*/;
    this.windows          = this.windows && (!this.ie || this.ieWin);
    this.ieMac            = this.ie && this.macintosh/*@cc_on && @_jscript_version < 5.1 @*/;
    this.macintosh        = this.macintosh && (!this.ie || this.ieMac);
    this.safari           = ua.indexOf('safari') > -1;
    this.webkit           = ua.indexOf('applewebkit') > -1 && !this.konqueror;
    this.khtml            = this.webkit || this.konqueror;
    this.gecko            = !this.webkit && product == 'gecko';

    this.operaVersion     = this.opera     && /.*opera(\s|\/)(\d+\.\d+)/.exec(ua) ? parseInt(RegExp.$2) : 0;
    this.webkitVersion    = this.webkit    && /.*applewebkit\/(\d+).*/.exec(ua)   ? parseInt(RegExp.$1) : 0;
    this.geckoBuildDate   = this.gecko     && /.*gecko\/(\d{8}).*/.exec(ua)       ? parseInt(RegExp.$1) : 0;
    this.konquerorVersion = this.konqueror && /.*konqueror\/(\d\.\d).*/.exec(ua)  ? parseInt(RegExp.$1) : 0;

    this.flashVersion     = 0;
    if(this.ieWin) {
      var axo;
      var stop = false;
      try {
	      axo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.7');
      } catch(e) {
        // In case the Flash 7 registry key does not exist, we need to test for specific 
        // Flash 6 installs before we can use the general key. 
        // See also <http://blog.deconcept.com/2006/01/11/getvariable-setvariable-crash-internet-explorer-flash-6/>.
        // Many thanks to Geoff Sterns and Bobby van der Sluis for clarifying problem and providing
        // examples of non-crashing code.
    		try {
    		  axo                   = new ActiveXObject('ShockwaveFlash.ShockwaveFlash.6');
      		this.flashVersion     = 6;
          axo.AllowScriptAccess = 'always';
        } catch(e) { stop = this.flashVersion == 6; }

				if(!stop) try { axo = new ActiveXObject('ShockwaveFlash.ShockwaveFlash');	} catch(e) {}
			}

      if(!stop && axo) this.flashVersion = parseFloat(/([\d,?]+)/.exec(axo.GetVariable('$version'))[1].replace(/,/g, '.'));
    } else if(navigator.plugins && navigator.plugins['Shockwave Flash']) {
      var flashPlugin = navigator.plugins['Shockwave Flash'];
      this.flashVersion = parseFloat(/(\d+\.?\d*)/.exec(flashPlugin.description)[1]);

      // Watch out for QuickTime, which could be stealing the Flash handling!
      var i = 0;
      while(this.flashVersion >= MIN_FLASH_VERSION && i < navigator.mimeTypes.length) {
        var mime = navigator.mimeTypes[i];
        if(mime.type == 'application/x-shockwave-flash' && mime.enabledPlugin.description.toLowerCase().indexOf('quicktime') > -1) {
          this.flashVersion = 0;
          this.quicktime = true;
        }
        i++;
      }
    }

    this.flash = this.flashVersion >= MIN_FLASH_VERSION;

    // There are other conditions, but these are ruled out in `computedStyledSupport` or `supported`.
    this.transparencySupport  = this.macintosh || this.windows;

    this.computedStyleSupport = this.ie || document.defaultView && document.defaultView.getComputedStyle 
      && (!this.gecko || this.geckoBuildDate >= 20030624); // Older Geckos have trouble with `getComputedStyle()`

    this.css = true;
    // Verify CSS support. We'll be changing the color of the head element, as it won't affect
    // page rendering and no other elements (other than HTML) can be relied up-on at this point.
    if(this.computedStyleSupport) {
      try { // Not sure if there are user agents which will disallow this
        var node = document.getElementsByTagName('head')[0];
        node.style.backgroundColor = '#FF0000';
        var color = dom.getComputedStyle(node, 'backgroundColor'); // Safari will return null, Konqueror an empty string
        this.css = !color || /\#F{2}0{4}|rgb\(255,\s?0,\s?0\)/i.test(color);
        node = null;
      } catch(e) {}
    }
    
    this.xhtmlSupport = !!window.DOMParser && !!document.importNode;
    this.innerHtmlSupport;
    try { 
      var n = dom.create('span');
      if(!this.ieMac) n.innerHTML = 'x'; // Need to test for IE/Mac, since the code will still be executed in IE/Mac.
      this.innerHtmlSupport = n.innerHTML == 'x';
    } catch (e) { this.innerHtmlSupport = false; }
    
    this.zoomSupport       = !!(this.opera && document.documentElement);
    this.geckoXml          = this.gecko && (document.contentType || '').indexOf('xml') > -1;

    this.requiresPrefetch  = this.ieWin || this.khtml;
    this.verifiedKonqueror = false;

    this.supported         = this.flash && this.css && (!this.ie || this.ieSupported) 
      && (!this.opera || this.operaVersion >= 8) && (!this.webkit || this.webkitVersion >= 412)  
      && (!this.konqueror || this.konquerorVersion > 3.5) && this.computedStyleSupport
      && (this.innerHtmlSupport || !this.khtml && this.xhtmlSupport);
  };
  this.ua = ua;

  var util = new function() {
    function capitalize($) {
      return $.toUpperCase();
    }
    
    this.normalize = function(str) {
      if(SIFR.preserveSingleWhitespace) return str.replace(/\s/g, SINGLE_WHITESPACE);
      return str.replace(/(\s)\s+/g, '$1');
    };
    
    this.textTransform = function(type, str) {
      switch(type) {
        case 'uppercase':
          str = str.toUpperCase();
          break;
        
        case 'lowercase':
          str = str.toLowerCase();
          break;
          
        case 'capitalize':
          var strCopy = str;
          str = str.replace(/^\w|\s\w/g, capitalize);
          if(str.indexOf('function capitalize') != -1) {
            var substrs = strCopy.replace(/(^|\s)(\w)/g, '$1$1$2$2').split(/^\w|\s\w/g);
            str = '';
            for(var i = 0; i < substrs.length; i++) str += substrs[i].charAt(0).toUpperCase() + substrs[i].substring(1);
          }
          break;
      }
      
      return str;
    };
    
    this.toHexString = function(str) {
      if(typeof(str) != 'string' || !str.charAt(0) == '#' || str.length != 4 && str.length != 7) return str;
      
      str = str.replace(/#/, '');
      if(str.length == 3) str = str.replace(/(.)(.)(.)/, '$1$1$2$2$3$3');

      return '0x' + str;
    };

    this.toJson = function(obj) {
      var json = '';

      switch(typeof(obj)) {
        case 'string':
          json = '"' + obj + '"';
          break;
        case 'number':
        case 'boolean':
          json = obj.toString();
          break;
        case 'object':
          json = [];
          for(var prop in obj) {
            if(obj[prop] == Object.prototype[prop]) continue;
            json.push('"' + prop + '":' + util.toJson(obj[prop]));
          }
          json = '{' + json.join(',') + '}';
          break;
      }

      return json;
    };
    
    this.convertCssArg = function(arg) {
      if(!arg) return {};
      if(typeof(arg) == 'object') {
        if(arg.constructor == Array) arg = arg.join('');
        else return arg;
      }

      var obj = {};
      var rules = arg.split('}');

      for(var i = 0; i < rules.length; i++) {
        var $ = rules[i].match(/([^\s{]+)\s*\{(.+)\s*;?\s*/);
        if(!$ || $.length != 3) continue;

        if(!obj[$[1]]) obj[$[1]] = {};

        var properties = $[2].split(';');
        for(var j = 0; j < properties.length; j++) {
          var $2 = properties[j].match(/\s*([^:\s]+)\s*\:\s*([^\s;]+)/);
          if(!$2 || $2.length != 3) continue;
          obj[$[1]][$2[1]] = $2[2];
        }
      }

      return obj;
    };

    this.extractFromCss = function(css, selector, property, remove) {
      var value = null;

      if(css && css[selector] && css[selector][property]) {
        value = css[selector][property];
        if(remove) delete css[selector][property];
      }

      return value;
    };
    
    this.cssToString = function(arg) {
      var css = [];
      for(var selector in arg) {
        var rule = arg[selector];
        if(rule == Object.prototype[selector]) continue;

        css.push(selector, '{');
        for(var property in rule) {
          if(rule[property] == Object.prototype[property]) continue;
          css.push(property, ':', rule[property], ';');
        }
        css.push('}');
      }

      return escape(css.join(''));
    };
  };
  this.util = util;

  var hacks = {};
  hacks.fragmentIdentifier = new function() {
    this.fix = true;

    var cachedTitle;
    this.cache = function() {
      cachedTitle = document.title;
    };

    function doFix() {
      document.title = cachedTitle;
    }

    this.restore = function() {
      if(this.fix) setTimeout(doFix, 0);
    };
  };

  // The zoom hack needs to be run before replace(). The synchronizer can be
  // used to ensure this.
  hacks.synchronizer = new function() {
    this.isBlocked = false;

    this.block = function() {
      this.isBlocked = true;
    };

    this.unblock = function() {
      this.isBlocked = false;
      blockedReplaceKwargsStore.replaceAll();
    };
  };

  // Detect the page zoom in Opera. Adapted from <http://virtuelvis.com/archives/2005/05/opera-measure-zoom>.
  hacks.zoom = new function() {
    // Latest zoom, assume 100
    var latestZoom = 100;

    this.getLatest = function() {
      return latestZoom;
    }

    if(ua.zoomSupport && ua.opera) {
      // Create the DOM element used to calculate the zoom.
      var node = document.createElement('div');
      node.style.position = 'fixed';
      node.style.left = '-65536px';
      node.style.top = '0';
      node.style.height = '100%';
      node.style.width = '1px';
      node.style.zIndex = '-32';
      document.documentElement.appendChild(node);

      function updateZoom() {
        if(!node) return;

        var zoom = window.innerHeight / node.offsetHeight;

        var correction = Math.round(zoom * 100) % 10;
        if(correction > 5) zoom = Math.round(zoom * 100) + 10 - correction;
        else zoom = Math.round(zoom * 100) - correction;

        latestZoom = isNaN(zoom) ? 100 : zoom;
        hacks.synchronizer.unblock();

        document.documentElement.removeChild(node);
        node = null;
      }

      hacks.synchronizer.block();

      // We need to wait a few ms before Opera the offsetHeight of the node
      // becomes available.
      setTimeout(updateZoom, 54);
    }
  };
  this.hacks = hacks;

  var replaceKwargsStore = {
    kwargs: [],
    replaceAll:  function() {
      for(var i = 0; i < this.kwargs.length; i++) SIFR.replace(this.kwargs[i]);
      this.kwargs = [];
    }
  };

  var blockedReplaceKwargsStore = {
    kwargs: [],
    replaceAll: replaceKwargsStore.replaceAll
  };

  // The goal here is not to prevent usage of the Flash movie, but running sIFR on possibly translated pages
  function isValidDomain() {
    if(SIFR.domains.length == 0) return true;

    var domain = '';
    try { // When trying to access document.domain on a Google-translated page with Firebug, I got an exception.
      domain = document.domain;
    } catch(e) {};

    if(SIFR.fromLocal && sIFR.domains[0] != 'localhost') sIFR.domains.unshift('localhost');

    for(var i = 0; i < SIFR.domains.length; i++) {
      if(SIFR.domains[i] == '*' || SIFR.domains[i] == domain) return true;
    }

    return false;
  }

  //=:public Activates sIFR if it is enabled, supported and on a valid domain.
  this.activate = function() {
    if(!ua.supported || !this.isEnabled || this.isActive || !isValidDomain()) return;

    this.isActive = true;

    if(this.hideElements) this.setFlashClass();

    if(ua.ieWin && hacks.fragmentIdentifier.fix && window.location.hash != '') {
      hacks.fragmentIdentifier.cache();
    } else hacks.fragmentIdentifier.fix = false;

    if(!this.registerEvents) return;

    function handler(evt) {
      SIFR.initialize();

      // Remove handlers to prevent memory leak in Firefox 1.5, but only after onload.
      if(evt && evt.type == 'load') {
        if(document.removeEventListener) {
          document.removeEventListener('DOMContentLoaded', handler, false);
          document.removeEventListener('load', handler, false);
        }
        if(window.removeEventListener) window.removeEventListener('load', handler, false);
      }
    }
    
    if(window.addEventListener) {
      // Opera and also Safari load JavaScript and CSS synchrously, so we can't rely on 
      // correct information to do the replacements. Hence, we only use DOMContentLoaded
      // for Gecko-based browsers. In other cases, add a normal load event to the document.
      //:note The load event might be redundant, needs testing!
      if(SIFR.useDomContentLoaded && ua.gecko) document.addEventListener('DOMContentLoaded', handler, false);
      window.addEventListener('load', handler, false);
    } else if(ua.ieWin) {
      if(SIFR.useDomContentLoaded && !hasPrefetched) { // Replacing before onload breaks prefetching
        document.write('<scr' + 'ipt id=__sifr_ie_onload defer src=//:><\/script>');
        document.getElementById('__sifr_ie_onload').onreadystatechange = function() {
          if(this.readyState == 'complete') {
            handler();
            this.removeNode();
          }
        };
      }
      window.attachEvent('onload', handler);
    }
  };

  /*=:public
      Sets the `CSS_HASFLASH` class to the body element if it exists, otherwise to the document
      element.
  */  
  this.setFlashClass = function() {
    if(this.hasFlashClassSet) return;

    dom.addClass(CSS_HASFLASH, dom.getBody() || document.documentElement);
    this.hasFlashClassSet = true;
  };

  //=:public Removes the `CSS_HASFLASH` class from the body and document element.
  this.removeFlashClass = function() {
    if(!this.hasFlashClassSet) return;

    dom.removeClass(CSS_HASFLASH, dom.getBody());
    dom.removeClass(CSS_HASFLASH, document.documentElement);
    this.hasFlashClassSet = false;
  }

  //=:public Initializes sIFR. Should only be called if `sIFR.registerEvents` is `false`.
  this.initialize = function() {
    if(isInitialized || !this.isActive || !this.isEnabled) return;

    isInitialized = true;
    replaceKwargsStore.replaceAll();
    clearPrefetch();
  };

  function getSource(src) {
    if(typeof(src) != 'string') {
      // This is a niciety to allow you to create general configuration objects
      // for the prefetch as well as the replacement. You could create constructs
      // like `{src: {src: { /*....*/ }}}`, but that's not really a problem.
      if(src.src) src = src.src;

      // It might be a string now...
      if(typeof(src) != 'string') {
        var versions = [];
        for(var version in src) if(src[version] != Object.prototype[version]) versions.push(version);
        versions.sort().reverse();

        var result = '';
        var i = -1;
        while(!result && ++i < versions.length) {
          if(parseFloat(versions[i]) <= ua.flashVersion) result = src[versions[i]];
        }
        
        src = result;
      }
    }
    
    if(!src && SIFR.debugMode) throw new Error("sIFR: Could not determine appropriate source");
    
    // Some IE installs refuse to show the Flash unless it gets the really absolute
    // URI of the file. I haven't been able to reproduce this behavior but let's
    // ensure a full URI none the less. This turns `/foo.swf` in `http://example.com/foo.swf`.
    if(ua.ie && src.charAt(0) == '/') {
      src = window.location.toString().replace(/([^:]+)(:\/?\/?)([^\/]+).*/, '$1$2$3') + src;
    }
    
    return src;
  }

  /*=:public
      Pre-fetch Flash movies. This stops unnecessary requests for Flash movies, makes sIFR
      load faster and saves bandwidth.
      
    =:arguments
      This function can be called with multiple arguments, which have to be of the following types:
      * String => The URI of the Flash movie to pre-fetch.
      * Object => Object in the form of `{src: …}` where `src` can be an object, as described below,
                  or a string, as described above.
      * Object => Object in the form of `{'n': string} where `n` is a Flash version and `string` 
                  is as described above.
  */
  this.prefetch = function(/* … */) {
    if(!ua.requiresPrefetch || !ua.supported || !this.isEnabled || !isValidDomain()) return;
    if(this.setPrefetchCookie && new RegExp(';?' + PREFETCH_COOKIE + '=true;?').test(document.cookie)) return;

    try { // We don't know which DOM actions the user agent will allow
      hasPrefetched = true;

      if(ua.ieWin) prefetchIexplore(arguments);
      else prefetchLight(arguments);

      if(this.setPrefetchCookie) document.cookie = PREFETCH_COOKIE + '=true;path=' + this.cookiePath;
    } catch(e) { if(SIFR.debugMode) throw e; }
  };

  function prefetchIexplore(args) {
    for(var i = 0; i < args.length; i++) {
      document.write('<embed src="' + getSource(args[i]) + '" sIFR-prefetch="true" style="display:none;">');
    }
  }

  function prefetchLight(args) {
    for(var i = 0; i < args.length; i++) new Image().src = getSource(args[i]);
  }

  function clearPrefetch() {
    if(!ua.ieWin || !hasPrefetched) return;

    try {
      var nodes = document.getElementsByTagName('embed');
      for(var i = nodes.length - 1; i >= 0; i--) {
        var node = nodes[i];
        if(node.getAttribute('sIFR-prefetch') == 'true') node.parentNode.removeChild(node);
      }
    } catch(e) {}
  }

  // Gives a font-size to required vertical space ratio
  // Tested with Verdana
  function getRatio(size) {
    if(size <= 10) return 1.55;
    if(size <= 19) return 1.45;
    if(size <= 32) return 1.35;
    if(size <= 71) return 1.30;
    return 1.25;
  }

  function getFilters(obj) {
    var filters = [];
    for(var filter in obj) {
      if(obj[filter] == Object.prototype[filter]) continue;

      var properties = obj[filter];
      filter = [filter.replace(/filter/i, '') + 'Filter'];

      for(var property in properties) {
        if(properties[property] == Object.prototype[property]) continue;
        filter.push(property + ':' + escape(util.toJson(util.toHexString(properties[property]))));
      }

      filters.push(filter.join(','));
    }

    return filters.join(';');
  }

  /*=:public
      Replaces elements with sIFR text.
      
    =:arguments
      * kwargs:Object => A [keyword argument] object.
      * (optional) mergeKwargs:Object => A [keyword argument] object. If present the properties
        of `kwargs` will be merged with this object.
    
    =:keyword arguments
      * elements:Array               => List of elements to be replaced. Useful if the elements 
                                        can't be selected by sIFR's `parseSelector`, or if you 
                                        wish to use a different selector parser.

      * selector:String              => Selector which will be parsed by `parseSelector` (if present).
                                        The selected elements will be replaced. Will not be used 
                                        if `elements` is specified.

      * css:Object                   => CSS to be applied to the content of the Flash movie. 
                                        Specify the CSS in the form of `{'selector': {'property': 'value'}}`.
                                        This is the preferred argument type. See also [CSS properties].

      * css:String                   => CSS to be applied to the content of the Flash movie.
                                        See also [CSS properties].

      * css:Array                    => CSS to be applied to the content of the Flash movie. 
                                        The array will be converted to a String and is then parsed as such.
                                        See also [CSS properties].

      * filters:Object               => Filters to be applied to the Flash movie. See also [Filters].

      * forceClear:Boolean           => As described in [sIFR.forceClear]. Overrides `sIFR.forceClear`.

      * fitExactly:Boolean           => As described in [sIFR.fitExactly]. Overrides `sIFR.fitExactly`.

      * forceWidth:Boolean           => As described in [sIFR.forceWidth]. Overrides `sIFR.forceWidth`.

      * tuneWidth:Number             => Positive or negative number which is added to the width of the Flash
                                        movie. This allows for tuning of the space surrounding the text.

      * tuneHeight:Number            => Positive or negative number which is added to the height of the Flash
                                        movie. This allows for tuning of the space surrounding the text.

      * offsetLeft:Number            => Positive or negative number which specifies the horizontal position 
                                        of the text inside the Flash movie.

      * offsetTop:Number             => Positive or negative number which specifies the vertical position 
                                        of the text inside the Flash movie.

      * wmode:String                 => Specifies the window mode of the Flash movie. When `transparent` 
                                        the movie will be transparent. When `opaque` elements can be 
                                        displayed on top of the movie. If transparency is not supported `wmode` 
                                        will be set to `opaque` and the background color specified in the CSS
                                        will be used. 
                                        Therefore, it is important to always specify a background color!

      * gridFitType:String           => See <http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002747.html>. 
                                        If the text is right aligned, `subpixel` will be used by default. Otherwise `pixel` will be used.

      * thickness:Number             => See <http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002788.html>.

      * sharpness:Number             => See <http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002787.html>.

      * blendMode:String             => See <http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002444.html>.

      * modifyCss:Function           => Callback function which allows you to modify the CSS object. 
                                        For example, you can base the styles on the computed style of 
                                        elements you want to replace. The following properties however 
                                        need to be specified in the `css` argument:
                                          * leading
                                          * background-color
                                          * opacity
                                          * blend-mode
                                          * text-align
                                          * text-transform
                                        The callback function is called with the following arguments:
                                          * css:Object          => The CSS object which contains rules 
                                                                   for rendering the Flash movie
                                          * contentNode:Element => The element currently being replaced
                                          * selector:String     => The `selector` argument, if specified.

      * modifyContent:Function       => Callback function which allows you to modify the element being 
                                        replaced. This allows you to make specific changes before the 
                                        element is replaced with the Flash movie. It has no effect on 
                                        the alternate content. The callback function is called with
                                        the following arguments:
                                          * contentNode:Element => The element currently being replaced
                                          * selector:String     => The `selector` argument, if specified.

      * modifyContentString:Function => Callback function which allows you to modify the HTML source 
                                        to be rendered in the Flash movie. 
                                        The callback function is called with the following arguments:
                                          * content:String  => The HTML source.
                                          * selector:String => The `selector` argument, if specified.

      * onReplacement:Function       => Event handler which fires when an element has been replaced.
                                        It's argument is the Flash element.

    =:css properties
      Flash supports a subset of CSS. The most important differences is that it only supports one class
      per element, and that there is only one way to encode colors. If you are replacing elements with
      more than one class name, sIFR will look for one prefixed with `sIFR-class`. If such a class 
      does not exist it uses the first class found. As an example, the class `sIFR-class-foo` is picked
      and converted to `foo`. The only way to encode the color red is `#FF0000`. Blue is `#0000FF`.
      
      In order to apply CSS properties to the entire Flash movie you need to use `.sIFR-root` as the
      selector. Other elements can be selected normally, so if the replaced text has an *em* element,
      you can select it using `em`. If an element does not show up try setting
      it's display to block.
      
      The following properties are special in that they are handled by sIFR, and not Flash. Please note
      that no shorthands exist for these properties:
        * background-color => Specifies the background color of the Flash movie. A value of `transparent` does *not* imply `wmode: 'transparent'`, as the specified background-color is used as a fallback in case transparency is not supported.
        * leading          => See <http://livedocs.macromedia.com/flash/8/main/00002812.html>.
        * kerning          => See <http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002811.html>.
        * opacity          => See <http://livedocs.macromedia.com/flash/8/main/wwhelp/wwhimpl/common/html/wwhelp.htm?context=LiveDocs_Parts&file=00002732.html>.
        * text-transform   => Specifes text transformation. This is handled by sIFR because Flash does not support this natively. It is applied to all text, if you want to apply it to a specific element you'll need to use the `modifyContent` function. If `sIFR.forceTextTransform` is `false`, sIFR will not perform the transformation.
      
      The following properties are supported by Flash natively:
        * text-align       => You can use `left`, `center`, `right`.
        * text-decoration  => You can use `none` and `underline`.
        * margin-left      => You can only use a number, without a unit.
        * margin-right     => You can only use a number, without a unit.
        * font-weight      => You can use `normal` and `bold`. Make sure you've embedded the font in bold if you want to use it.
        * font-style       => You can use `normal` and `italic`. Make sure you've embedded the font in italic if you want to use it.
        * text-indent      => You can only use a number, without a unit.
        * color            => As explained above.
        * display          => You can use `inline`, `block` and `none`.

      Use the following properties at your own risk:
        * font-size        => You can only use a number, without a unit. Overriden for `.sIFR-root`.
        * font-family
    
    =:filters
      Flash 8 supports a number of filters. These can be used in sIFR as well. Each property of the
      `filters` keyword argument describes a filter. As the property name you use the filter name,
      for example `DropShadow`. This name is *case-sensitive* [1]! The description is in the form of
      another object, where each property corresponds with a filter property. So, for example:
      
        filters: {
          DropShadow: {
            knockout: true
            ,distance: 1
            ,color: '#330000'
            ,strength: 2
          }
        }
        
      The colors can be specified both as a hexadecimal string (`'#330000'`) or as a hexadecimal
      number (`0x330000`). Again, the property names are case-sensitive.
      
      For more information go to <http://wiki.novemberborn.net/sifr3/Filters>.
      
      [1]: Alternatively, you can append 'Filter' to the name, so `DropShadow` becomes `DropShadowFilter`.
  */
  this.replace = function(kwargs, mergeKwargs) {
    if(!ua.supported) return;
    
    // This lets you specify to kwarg objects so you don't have to repeat common settings.
    // The first object will be merged with the second, while properties in the second 
    // object have priority over those in the first. The first object is unmodified
    // for further use, the resulting second object will be used in the replacement.
    if(mergeKwargs) {
      for(var property in kwargs) {
        if(typeof(mergeKwargs[property]) == 'undefined') mergeKwargs[property] = kwargs[property];
      }
      kwargs = mergeKwargs;
    }
    
    if(!isInitialized) return replaceKwargsStore.kwargs.push(kwargs);
    if(hacks.synchronizer.isBlocked) return blockedReplaceKwargsStore.kwargs.push(kwargs);

    var nodes = kwargs.elements;
    if(!nodes && parseSelector) nodes = parseSelector(kwargs.selector);
    if(nodes.length == 0) return;

    this.setFlashClass();

    var src = getSource(kwargs.src);
    var css = util.convertCssArg(kwargs.css);
    var filters = getFilters(kwargs.filters);
    
    var forceClear = (kwargs.forceClear == null) ? SIFR.forceClear : kwargs.forceClear;
    var fitExactly = (kwargs.fitExactly == null) ? SIFR.fitExactly : kwargs.fitExactly;
    var forceWidth = fitExactly || (kwargs.forceWidth == null ? SIFR.forceWidth : kwargs.forceWidth);

    var leading = parseInt(util.extractFromCss(css, '.sIFR-root', 'leading')) || 0;
    var backgroundColor = util.extractFromCss(css, '.sIFR-root', 'background-color', true) || '#FFFFFF';
    var opacity = util.extractFromCss(css, '.sIFR-root', 'opacity', true) || '100';
    if(parseFloat(opacity) < 1) opacity = 100 * parseFloat(opacity); // Make sure to support percentages and decimals
    var kerning = util.extractFromCss(css, '.sIFR-root', 'kerning', true) || '';
    var gridFitType = kwargs.gridFitType || util.extractFromCss(css, '.sIFR-root', 'text-align') == 'right' ? 'subpixel' : 'pixel';
    var textTransform = SIFR.forceTextTransform ? util.extractFromCss(css, '.sIFR-root', 'text-transform', true) || 'none' : 'none';

    var cssText = '';
    // Alignment is handled by the browser in this case.
    if(fitExactly) util.extractFromCss(css, '.sIFR-root', 'text-align', true);
    if(!kwargs.modifyCss) cssText = util.cssToString(css);

    var wmode = kwargs.wmode || '';
    if(wmode == 'transparent') {
			if(!ua.transparencySupport)	wmode = 'opaque';
			else backgroundColor = 'transparent';
		}

    for(var i = 0; i < nodes.length; i++) {
      var node = nodes[i];

      if(!ua.verifiedKonqueror) {
        if(dom.getComputedStyle(node, 'lineHeight').match(/e\+08px/)) {
          ua.supported = SIFR.isEnabled = false;
          this.removeFlashClass();
          return;
        }
        ua.verifiedKonqueror = true;
      }

      if(dom.hasClass(CSS_REPLACED, node) || dom.hasClass(CSS_IGNORE, node)) continue;

      // Elements with no height (`0` in IE, `undefined` in Safari) are usually `display: none`.
      // Let's attempt to display them and replace them anyway.
      var resetDisplay = false;

      // Without a height or width, we can't function. Tring again with display:block…
      // See also <http://www.snook.ca/archives/javascript/safari2_display-none_getcomputedstyle/>.
      if(!node.offsetHeight ||!node.offsetWidth) {
        if(!SIFR.replaceNonDisplayed) continue;

        node.style.display = 'block';
        if(!node.offsetHeight || !node.offsetWidth) { // If they are still without height or width, don't replace them.
          node.style.display = '';
          continue;
        }
        resetDisplay = true;
      }

      if(forceClear && ua.gecko) node.style.clear = 'both';

      // If the text doesn't wrap nicely, the width becomes too large and Flash
      // can't adjust for it. By setting the text to just "X" we can be sure
      // we get the correct width.
      var html = null;
      if(SIFR.fixWrap && ua.ie && dom.getComputedStyle(node, 'display') == 'block') {
        html = node.innerHTML;
        dom.setInnerHtml(node, 'X');
      }

      // Get the width (again to approximate the final size). The computed width
      // may not be a pixel unit in IE, in which case we try to calculate using
      // padding and borders and the offsetWidth.
      var width = dom.getStyleAsInt(node, 'width', ua.ie);
      if(ua.ie && width == 0) {
        var paddingRight  = dom.getStyleAsInt(node, 'paddingRight', true);
        var paddingLeft   = dom.getStyleAsInt(node, 'paddingLeft', true);
        var borderRight   = dom.getStyleAsInt(node, 'borderRightWidth', true);
        var borderLeft    = dom.getStyleAsInt(node, 'borderLeftWidth', true);
        width = node.offsetWidth - paddingLeft - paddingRight - borderLeft - borderRight;
      }

      if(html && SIFR.fixWrap && ua.ie) dom.setInnerHtml(node, html);
      
      var lineHeight, lines;
      if(!ua.ie) { //:=todo Only do once for each selector?
        lineHeight = dom.getStyleAsInt(node, 'lineHeight');
        lines = Math.floor(dom.getStyleAsInt(node, 'height') / lineHeight);
      } else if(ua.ie) { // IE returs computed style in the original units, which is quite useless.
        var html = node.innerHTML;

        // Without these settings, we won't be able to get the rects properly. getClientRects()
        // won't work on elements having layout or that are hidden.
        node.style.visibility  = 'visible';
        node.style.overflow    = 'visible';
        node.style.position    = 'static';
        node.style.zoom        = 'normal';
        node.style.writingMode = 'lr-tb';
        node.style.width       = node.style.height = 'auto';
        node.style.maxWidth    = node.style.maxHeight = node.style.styleFloat  = 'none';
                
        var rectNode = node;
        var hasLayout = node.currentStyle.hasLayout;
        if(hasLayout) {
          dom.setInnerHtml(node, '<div class="' + CSS_LAYOUT + '">X<br />X<br />X</div>');
          rectNode = node.firstChild;
        } else dom.setInnerHtml(node, 'X<br />X<br />X');

        var rects = rectNode.getClientRects();
        lineHeight = rects[1].bottom - rects[1].top;

        // In IE, the lineHeight is about 1.25 times the height in other browsers.
        lineHeight = Math.ceil(lineHeight * 0.8);

        if(hasLayout) {
          dom.setInnerHtml(node, '<div class="' + CSS_LAYOUT + '">' + html + '</div>');
          rectNode = node.firstChild;
        } else dom.setInnerHtml(node, html);
        rects = rectNode.getClientRects();
        lines = rects.length;
        
        if(hasLayout) dom.setInnerHtml(node, html);

        // By setting an empty string, the values will fall back to those in the (non-inline) CSS.
        // When that CSS changes, the changes are reflected here. Setting explicit values would break
        // that behaviour.
        node.style.visibility = node.style.width = node.style.height = node.style.maxWidth 
        = node.style.maxHeight = node.style.overflow = node.style.styleFloat
        = node.style.position = node.style.zoom = node.style.writingMode = '';
      }

      // We have all the info we need, reset the display setting now.
      if(resetDisplay) node.style.display = '';
      if(forceClear && ua.gecko) node.style.clear = '';

      lineHeight = Math.max(MIN_FONT_SIZE, lineHeight);
      lineHeight = Math.min(MAX_FONT_SIZE, lineHeight);

      if(isNaN(lines) || !isFinite(lines)) lines = 1;
      var height = Math.round(lines * lineHeight);

      if(lines > 1 && leading) height += Math.round((lines - 1) * leading);

      // I wanted to use `noembed` here, but unfortunately FlashBlock only works with `span.sIFR-alternate`
      var alternate = dom.create('span');
      alternate.className = CSS_ALTERNATE;

      // Clone the original content to the alternate element.
      var contentNode = node.cloneNode(true);
      for(var j = 0, l = contentNode.childNodes.length; j < l; j++) {
        alternate.appendChild(contentNode.childNodes[j].cloneNode(true));
      }

      // Allow the sIFR content to be modified
      if(kwargs.modifyContent) kwargs.modifyContent(contentNode, kwargs.selector);
      if(kwargs.modifyCss) cssText = kwargs.modifyCss(css, contentNode, kwargs.selector);

      var content = handleContent(contentNode, textTransform);
      if(kwargs.modifyContentString) content = kwargs.modifyContentString(content, kwargs.selector);
      if(content == '') continue;
      var vars = ['content=' + content.replace(/\</g, '&lt;').replace(/>/g, '&gt;'),
                  'width=' + width, 'height=' + height, 'fitexactly=' + (fitExactly ? 'true' : ''),
                  'tunewidth=' + (kwargs.tuneWidth || ''), 'tuneheight=' + (kwargs.tuneHeight || ''),
                  'offsetleft=' + (kwargs.offsetLeft || ''), 'offsettop=' + (kwargs.offsetTop || ''),
                  'thickness=' + (kwargs.thickness || ''), 'sharpness=' + (kwargs.sharpness || ''), 
                  'kerning=' + kerning, 'gridfittype=' + gridFitType, 'zoomsupport=' + ua.zoomSupport, 
                  'filters=' + filters, 'opacity=' + opacity, 'blendmode=' + (kwargs.blendMode || ''), 
                  'size=' + lineHeight, 'zoom=' + dom.getZoom(), 'css=' + cssText];
      vars = encodeURI(vars.join('&amp;'));

      var callbackName = 'sIFR_callback_' + elementCount++;
      var callbackInfo = {flashNode: null};
      window[callbackName + '_DoFSCommand'] = (function(callbackInfo) {
        return function(info, arg) {
          if(/(FSCommand\:)?resize/.test(info)) {
            var $ = arg.split(':');
            callbackInfo.flashNode.setAttribute($[0], $[1]);
            // Here comes another story!
            //
            // Good old Safari (saw this in 2.0.3) will *not* repaint the 
            // Flash movie with the new dimensions *until* the document
            // receives a UIEvent. I haven't tested this throroughly, so it
            // might respond to other events as well.
            //
            // The solution is to trick Safari into thinking that the `embed`
            // element has changed, this is done by adding an empty string
            // to it's `innerHTML`. Be aware though that adding this string
            // to `node` (the parent of the `embed` element) will immediately
            // crash Safari.
            //
            // Just to be sure this hack is applied to all browsers which
            // implement the KHTML engine.
            //
            /*:=todo Test this bug in older browsers to see if it occurs there
                      as well.
            */
            if(ua.khtml) callbackInfo.flashNode.innerHTML += '';
          }
        }
      })(callbackInfo);

      // Approach the final height to avoid annoying movements of the page
      height = Math.round(lines * getRatio(lineHeight) * lineHeight);

      var forcedWidth = forceWidth ? width : '100%';

      var flash;
      if(ua.ie) {
        flash = [
          '<object classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" id="', callbackName,
           '" sifr="true" width="', forcedWidth, '" height="', height, '" class="', CSS_FLASH, '">',
            '<param name="movie" value="', src, '"></param>',
            '<param name="flashvars" value="', vars, '"></param>',
            '<param name="allowScriptAccess" value="always"></param>',
            '<param name="quality" value="best"></param>',
            '<param name="wmode" value="', wmode, '"></param>',
            '<param name="bgcolor" value="', backgroundColor, '"></param>',
            '<param name="name" value="', callbackName, '"></param>',
          '</object>',
          // Load in the callback code. Keep the <script> line exactly the same!!! (Yes, IE is that crappy)
          // Thanks to Tom Lee for the tip: <http://tom-lee.blogspot.com/2006/04/dynamically-inserting-fscommand.html>
          '<scr', 'ipt event=FSCommand(info,args) for=', callbackName, '>', 
            callbackName, '_DoFSCommand(info, args);',
          '</', 'script>' // End like this to prevent syntax error in IE/Mac.
        ].join('');
      } else {
        flash = [
          '<embed class="', CSS_FLASH, '" type="application/x-shockwave-flash" src="', 
          src,'" quality="best" flashvars="', vars, '" width="', forcedWidth, '" height="', height,
          '" wmode="', wmode, '" bgcolor="', backgroundColor, '" name="', callbackName,
          '" allowScriptAccess="always" sifr="true"></embed>'
        ].join('');
      }

      dom.setInnerHtml(node, flash);
      callbackInfo.flashNode = node.firstChild;
      node.appendChild(alternate);
      dom.addClass(CSS_REPLACED, node);
      
      if(kwargs.onReplacement) kwargs.onReplacement(callbackInfo.flashNode);
    }

    hacks.fragmentIdentifier.restore();
  };

  /*=:private
    Walks through the childNodes of `source`. Generates a text representation of these childNodes.

    Returns:
    * string: the text representation.

    Notes:
    * A number of items are still to do. See the individual comments for that.
    * This method does not recursion because it'll be necessary to keep a 
      count of all links and their URIs. This is easier without recursion.
  */
  function handleContent(source, textTransform) {
    var stack = [], content = [];
    var nodes = source.childNodes;

    var i = 0;
    while(i < nodes.length) {
      var node = nodes[i];

      if(node.nodeType == 3) {
        var text = util.normalize(node.nodeValue);
        text = util.textTransform(textTransform, text);
        // Escape reserved characters
        content.push(text.replace(/\%/g, '%25').replace(/\&/g, '%26').replace(/\,/g, '%2C').replace(/\+/g, '%2B'));
      }

      if(node.nodeType == 1) {
        var attributes = [];
        var nodeName = node.nodeName.toLowerCase();

        var className = node.className || '';
        // If there are multiple classes, look for the specified sIFR class
        if(/\s+/.test(className)) {
          if(className.indexOf(CSS_CLASS)) className = className.match('(\\s|^)' + CSS_CLASS + '-([^\\s$]*)(\\s|$)')[2];
          // or use the first class
          else className = className.match(/^([^\s]+)/)[1];
        }
        if(className != '') attributes.push('class="' + className + '"');

        if(nodeName == 'a') {
          var href = node.getAttribute('href') || '';
          var target = node.getAttribute('target') || '';
          attributes.push('href="' + href + '"', 'target="' + target + '"');
        }

        content.push('<' + nodeName + (attributes.length > 0 ? ' ' : '') + escape(attributes.join(' ')) + '>');

        if(node.hasChildNodes()) {
          // Push the current index to the stack and prepare to iterate
          // over the childNodes.
          stack.push(i);
          i = 0;
          nodes = node.childNodes;
          continue;
        } else if(!/^(br|img)$/i.test(node.nodeName)) content.push('</', node.nodeName.toLowerCase(), '>');
      }

      if(stack.length > 0 && !node.nextSibling) {
        // Iterating the childNodes has been completed. Go back to the position
        // before we started the iteration. If that position was the last child,
        // go back even further.
        do {
          i = stack.pop();
          nodes = node.parentNode.parentNode.childNodes;
          node = nodes[i];
          if(node) content.push('</', node.nodeName.toLowerCase(), '>');
        } while(i < nodes.length && stack.length > 0);
      }

      i++;
    }
  
    return content.join('').replace(/\n|\r/g, '');
  }
};
