/*!
 * Copyright (c) 2010 Simo Kinnunen.
 * Licensed under the MIT license.
 *
 * @version ${Version}
 */

var Cufon = (function() {

 var api = function() {
  return api.replace.apply(null, arguments);
 };

 var DOM = api.DOM = {

  ready: (function() {

   var complete = false, readyStatus = { loaded: 1, complete: 1 };

   var queue = [], perform = function() {
    if (complete) return;
    complete = true;
    for (var fn; fn = queue.shift(); fn());
   };

   // Gecko, Opera, WebKit r26101+

   if (document.addEventListener) {
    document.addEventListener('DOMContentLoaded', perform, false);
    window.addEventListener('pageshow', perform, false); // For cached Gecko pages
   }

   // Old WebKit, Internet Explorer

   if (!window.opera && document.readyState) (function() {
    readyStatus[document.readyState] ? perform() : setTimeout(arguments.callee, 10);
   })();

   // Internet Explorer

   if (document.readyState && document.createStyleSheet) (function() {
    try {
     document.body.doScroll('left');
     perform();
    }
    catch (e) {
     setTimeout(arguments.callee, 1);
    }
   })();

   addEvent(window, 'load', perform); // Fallback

   return function(listener) {
    if (!arguments.length) perform();
    else complete ? listener() : queue.push(listener);
   };

  })(),

  root: function() {
   return document.documentElement || document.body;
  }

 };

 var CSS = api.CSS = {

  Size: function(value, base) {

   this.value = parseFloat(value);
   this.unit = String(value).match(/[a-z%]*$/)[0] || 'px';

   this.convert = function(value) {
    return value / base * this.value;
   };

   this.convertFrom = function(value) {
    return value / this.value * base;
   };

   this.toString = function() {
    return this.value + this.unit;
   };

  },

  addClass: function(el, className) {
   var current = el.className;
   el.className = current + (current && ' ') + className;
   return el;
  },

  color: cached(function(value) {
   var parsed = {};
   parsed.color = value.replace(/^rgba\((.*?),\s*([\d.]+)\)/, function($0, $1, $2) {
    parsed.opacity = parseFloat($2);
    return 'rgb(' + $1 + ')';
   });
   return parsed;
  }),

  // has no direct CSS equivalent.
  // @see http://msdn.microsoft.com/en-us/library/system.windows.fontstretches.aspx
  fontStretch: cached(function(value) {
   if (typeof value == 'number') return value;
   if (/%$/.test(value)) return parseFloat(value) / 100;
   return {
    'ultra-condensed': 0.5,
    'extra-condensed': 0.625,
    condensed: 0.75,
    'semi-condensed': 0.875,
    'semi-expanded': 1.125,
    expanded: 1.25,
    'extra-expanded': 1.5,
    'ultra-expanded': 2
   }[value] || 1;
  }),

  getStyle: function(el) {
   var view = document.defaultView;
   if (view && view.getComputedStyle) return new Style(view.getComputedStyle(el, null));
   if (el.currentStyle) return new Style(el.currentStyle);
   return new Style(el.style);
  },

  gradient: cached(function(value) {
   var gradient = {
    id: value,
    type: value.match(/^-([a-z]+)-gradient\(/)[1],
    stops: []
   }, colors = value.substr(value.indexOf('(')).match(/([\d.]+=)?(#[a-f0-9]+|[a-z]+\(.*?\)|[a-z]+)/ig);
   for (var i = 0, l = colors.length, stop; i < l; ++i) {
    stop = colors[i].split('=', 2).reverse();
    gradient.stops.push([ stop[1] || i / (l - 1), stop[0] ]);
   }
   return gradient;
  }),

  quotedList: cached(function(value) {
   // doesn't work properly with empty quoted strings (""), but
   // it's not worth the extra code.
   var list = [], re = /\s*((["'])([\s\S]*?[^\\])\2|[^,]+)\s*/g, match;
   while (match = re.exec(value)) list.push(match[3] || match[1]);
   return list;
  }),

  recognizesMedia: cached(function(media) {
   var el = document.createElement('style'), sheet, container, supported;
   el.type = 'text/css';
   el.media = media;
   try { // this is cached anyway
    el.appendChild(document.createTextNode('/**/'));
   } catch (e) {}
   container = elementsByTagName('head')[0];
   container.insertBefore(el, container.firstChild);
   sheet = (el.sheet || el.styleSheet);
   supported = sheet && !sheet.disabled;
   container.removeChild(el);
   return supported;
  }),

  removeClass: function(el, className) {
   var re = RegExp('(?:^|\\s+)' + className +  '(?=\\s|$)', 'g');
   el.className = el.className.replace(re, '');
   return el;
  },

  supports: function(property, value) {
   var checker = document.createElement('span').style;
   if (checker[property] === undefined) return false;
   checker[property] = value;
   return checker[property] === value;
  },

  textAlign: function(word, style, position, wordCount) {
   if (style.get('textAlign') == 'right') {
    if (position > 0) word = ' ' + word;
   }
   else if (position < wordCount - 1) word += ' ';
   return word;
  },

  textShadow: cached(function(value) {
   if (value == 'none') return null;
   var shadows = [], currentShadow = {}, result, offCount = 0;
   var re = /(#[a-f0-9]+|[a-z]+\(.*?\)|[a-z]+)|(-?[\d.]+[a-z%]*)|,/ig;
   while (result = re.exec(value)) {
    if (result[0] == ',') {
     shadows.push(currentShadow);
     currentShadow = {};
     offCount = 0;
    }
    else if (result[1]) {
     currentShadow.color = result[1];
    }
    else {
     currentShadow[[ 'offX', 'offY', 'blur' ][offCount++]] = result[2];
    }
   }
   shadows.push(currentShadow);
   return shadows;
  }),

  textTransform: (function() {
   var map = {
    uppercase: function(s) {
     return s.toUpperCase();
    },
    lowercase: function(s) {
     return s.toLowerCase();
    },
    capitalize: function(s) {
     return s.replace(/(?:^|\s)./g, function($0) {
      return $0.toUpperCase();
     });
    }
   };
   return function(text, style) {
    var transform = map[style.get('textTransform')];
    return transform ? transform(text) : text;
   };
  })(),

  whiteSpace: (function() {
   var ignore = {
    inline: 1,
    'inline-block': 1,
    'run-in': 1
   };
   var wsStart = /^\s+/, wsEnd = /\s+$/;
   return function(text, style, node, previousElement, simple) {
    if (simple) return text.replace(wsStart, '').replace(wsEnd, ''); // @fixme too simple
    if (previousElement) {
     if (previousElement.nodeName.toLowerCase() == 'br') {
      text = text.replace(wsStart, '');
     }
    }
    if (ignore[style.get('display')]) return text;
    if (!node.previousSibling) text = text.replace(wsStart, '');
    if (!node.nextSibling) text = text.replace(wsEnd, '');
    return text;
   };
  })()

 };

 CSS.ready = (function() {

  // don't do anything in Safari 2 (it doesn't recognize any media type)
  var complete = !CSS.recognizesMedia('all'), hasLayout = false;

  var queue = [], perform = function() {
   complete = true;
   for (var fn; fn = queue.shift(); fn());
  };

  var links = elementsByTagName('link'), styles = elementsByTagName('style');

  function isContainerReady(el) {
   return el.disabled || isSheetReady(el.sheet, el.media || 'screen');
  }

  function isSheetReady(sheet, media) {
   // in Opera sheet.disabled is true when it's still loading,
   // even though link.disabled is false. they stay in sync if
   // set manually.
   if (!CSS.recognizesMedia(media || 'all')) return true;
   if (!sheet || sheet.disabled) return false;
   try {
    var rules = sheet.cssRules, rule;
    if (rules) {
     // needed for Safari 3 and Chrome 1.0.
     // in standards-conforming browsers cssRules contains @-rules.
     // Chrome 1.0 weirdness: rules[<number larger than .length - 1>]
     // returns the last rule, so a for loop is the only option.
     search: for (var i = 0, l = rules.length; rule = rules[i], i < l; ++i) {
      switch (rule.type) {
       case 2: // @charset
        break;
       case 3: // @import
        if (!isSheetReady(rule.styleSheet, rule.media.mediaText)) return false;
        break;
       default:
        // only @charset can precede @import
        break search;
      }
     }
    }
   }
   catch (e) {} // probably a style sheet from another domain
   return true;
  }

  function allStylesLoaded() {
   // Internet Explorer's style sheet model, there's no need to do anything
   if (document.createStyleSheet) return true;
   // standards-compliant browsers
   var el, i;
   for (i = 0; el = links[i]; ++i) {
    if (el.rel.toLowerCase() == 'stylesheet' && !isContainerReady(el)) return false;
   }
   for (i = 0; el = styles[i]; ++i) {
    if (!isContainerReady(el)) return false;
   }
   return true;
  }

  DOM.ready(function() {
   // getComputedStyle returns null in Gecko if used in an iframe with display: none
   if (!hasLayout) hasLayout = CSS.getStyle(document.body).isUsable();
   if (complete || (hasLayout && allStylesLoaded())) perform();
   else setTimeout(arguments.callee, 10);
  });

  return function(listener) {
   if (complete) listener();
   else queue.push(listener);
  };

 })();

 function Font(data) {

  var face = this.face = data.face, wordSeparators = {
   '\u0020': 1,
   '\u00a0': 1,
   '\u3000': 1
  };

  this.glyphs = (function(glyphs) {
   var key, fallbacks = {
    '\u2011': '\u002d',
    '\u00ad': '\u2011'
   };
   for (key in fallbacks) {
    if (!hasOwnProperty(fallbacks, key)) continue;
    if (!glyphs[key]) glyphs[key] = glyphs[fallbacks[key]];
   }
   return glyphs;
  })(data.glyphs);

  this.w = data.w;
  this.baseSize = parseInt(face['units-per-em'], 10);

  this.family = face['font-family'].toLowerCase();
  this.weight = face['font-weight'];
  this.style = face['font-style'] || 'normal';

  this.viewBox = (function () {
   var parts = face.bbox.split(/\s+/);
   var box = {
    minX: parseInt(parts[0], 10),
    minY: parseInt(parts[1], 10),
    maxX: parseInt(parts[2], 10),
    maxY: parseInt(parts[3], 10)
   };
   box.width = box.maxX - box.minX;
   box.height = box.maxY - box.minY;
   box.toString = function() {
    return [ this.minX, this.minY, this.width, this.height ].join(' ');
   };
   return box;
  })();

  this.ascent = -parseInt(face.ascent, 10);
  this.descent = -parseInt(face.descent, 10);

  this.height = -this.ascent + this.descent;

  this.spacing = function(chars, letterSpacing, wordSpacing) {
   var glyphs = this.glyphs, glyph,
    kerning, k,
    jumps = [],
    width = 0, w,
    i = -1, j = -1, chr;
   while (chr = chars[++i]) {
    glyph = glyphs[chr] || this.missingGlyph;
    if (!glyph) continue;
    if (kerning) {
     width -= k = kerning[chr] || 0;
     jumps[j] -= k;
    }
    w = glyph.w;
    if (isNaN(w)) w = +this.w; // may have been a String in old fonts
    if (w > 0) {
     w += letterSpacing;
     if (wordSeparators[chr]) w += wordSpacing;
    }
    width += jumps[++j] = ~~w; // get rid of decimals
    kerning = glyph.k;
   }
   jumps.total = width;
   return jumps;
  };

 }

 function FontFamily() {

  var styles = {}, mapping = {
   oblique: 'italic',
   italic: 'oblique'
  };

  this.add = function(font) {
   (styles[font.style] || (styles[font.style] = {}))[font.weight] = font;
  };

  this.get = function(style, weight) {
   var weights = styles[style] || styles[mapping[style]]
    || styles.normal || styles.italic || styles.oblique;
   if (!weights) return null;
   // we don't have to worry about "bolder" and "lighter"
   // because IE's currentStyle returns a numeric value for it,
   // and other browsers use the computed value anyway
   weight = {
    normal: 400,
    bold: 700
   }[weight] || parseInt(weight, 10);
   if (weights[weight]) return weights[weight];
   // http://www.w3.org/TR/CSS21/fonts.html#propdef-font-weight
   // Gecko uses x99/x01 for lighter/bolder
   var up = {
    1: 1,
    99: 0
   }[weight % 100], alts = [], min, max;
   if (up === undefined) up = weight > 400;
   if (weight == 500) weight = 400;
   for (var alt in weights) {
    if (!hasOwnProperty(weights, alt)) continue;
    alt = parseInt(alt, 10);
    if (!min || alt < min) min = alt;
    if (!max || alt > max) max = alt;
    alts.push(alt);
   }
   if (weight < min) weight = min;
   if (weight > max) weight = max;
   alts.sort(function(a, b) {
    return (up
     ? (a >= weight && b >= weight) ? a < b : a > b
     : (a <= weight && b <= weight) ? a > b : a < b) ? -1 : 1;
   });
   return weights[alts[0]];
  };

 }

 function HoverHandler() {

  function contains(node, anotherNode) {
   try {
    if (node.contains) return node.contains(anotherNode);
    return node.compareDocumentPosition(anotherNode) & 16;
   }
   catch(e) {} // probably a XUL element such as a scrollbar
   return false;
  }

  function onOverOut(e) {
   var related = e.relatedTarget;
   // there might be no relatedTarget if the element is right next
   // to the window frame
   if (related && contains(this, related)) return;
   trigger(this, e.type == 'mouseover');
  }

  function onEnterLeave(e) {
   trigger(this, e.type == 'mouseenter');
  }

  function trigger(el, hoverState) {
   // A timeout is needed so that the event can actually "happen"
   // before replace is triggered. This ensures that styles are up
   // to date.
   setTimeout(function() {
    var options = sharedStorage.get(el).options;
    api.replace(el, hoverState ? merge(options, options.hover) : options, true);
   }, 10);
  }

  this.attach = function(el) {
   if (el.onmouseenter === undefined) {
    addEvent(el, 'mouseover', onOverOut);
    addEvent(el, 'mouseout', onOverOut);
   }
   else {
    addEvent(el, 'mouseenter', onEnterLeave);
    addEvent(el, 'mouseleave', onEnterLeave);
   }
  };

 }

 function ReplaceHistory() {

  var list = [], map = {};

  function filter(keys) {
   var values = [], key;
   for (var i = 0; key = keys[i]; ++i) values[i] = list[map[key]];
   return values;
  }

  this.add = function(key, args) {
   map[key] = list.push(args) - 1;
  };

  this.repeat = function() {
   var snapshot = arguments.length ? filter(arguments) : list, args;
   for (var i = 0; args = snapshot[i++];) api.replace(args[0], args[1], true);
  };

 }

 function Storage() {

  var map = {}, at = 0;

  function identify(el) {
   return el.cufid || (el.cufid = ++at);
  }

  this.get = function(el) {
   var id = identify(el);
   return map[id] || (map[id] = {});
  };

 }

 function Style(style) {

  var custom = {}, sizes = {};

  this.extend = function(styles) {
   for (var property in styles) {
    if (hasOwnProperty(styles, property)) custom[property] = styles[property];
   }
   return this;
  };

  this.get = function(property) {
   return custom[property] != undefined ? custom[property] : style[property];
  };

  this.getSize = function(property, base) {
   return sizes[property] || (sizes[property] = new CSS.Size(this.get(property), base));
  };

  this.isUsable = function() {
   return !!style;
  };

 }

 function addEvent(el, type, listener) {
  if (el.addEventListener) {
   el.addEventListener(type, listener, false);
  }
  else if (el.attachEvent) {
   el.attachEvent('on' + type, function() {
    return listener.call(el, window.event);
   });
  }
 }

 function attach(el, options) {
  var storage = sharedStorage.get(el);
  if (storage.options) return el;
  if (options.hover && options.hoverables[el.nodeName.toLowerCase()]) {
   hoverHandler.attach(el);
  }
  storage.options = options;
  return el;
 }

 function cached(fun) {
  var cache = {};
  return function(key) {
   if (!hasOwnProperty(cache, key)) cache[key] = fun.apply(null, arguments);
   return cache[key];
  };
 }

 function getFont(el, style) {
  var families = CSS.quotedList(style.get('fontFamily').toLowerCase()), family;
  for (var i = 0; family = families[i]; ++i) {
   if (fonts[family]) return fonts[family].get(style.get('fontStyle'), style.get('fontWeight'));
  }
  return null;
 }

 function elementsByTagName(query) {
  return document.getElementsByTagName(query);
 }

 function hasOwnProperty(obj, property) {
  return obj.hasOwnProperty(property);
 }

 function merge() {
  var merged = {}, arg, key;
  for (var i = 0, l = arguments.length; arg = arguments[i], i < l; ++i) {
   for (key in arg) {
    if (hasOwnProperty(arg, key)) merged[key] = arg[key];
   }
  }
  return merged;
 }

 function process(font, text, style, options, node, el) {
  var fragment = document.createDocumentFragment(), processed;
  if (text === '') return fragment;
  var separate = options.separate;
  var parts = text.split(separators[separate]), needsAligning = (separate == 'words');
  if (needsAligning && HAS_BROKEN_REGEXP) {
   // @todo figure out a better way to do this
   if (/^\s/.test(text)) parts.unshift('');
   if (/\s$/.test(text)) parts.push('');
  }
  for (var i = 0, l = parts.length; i < l; ++i) {
   processed = engines[options.engine](font,
    needsAligning ? CSS.textAlign(parts[i], style, i, l) : parts[i],
    style, options, node, el, i < l - 1);
   if (processed) fragment.appendChild(processed);
  }
  return fragment;
 }

 function replaceElement(el, options) {
  var name = el.nodeName.toLowerCase();
  if (options.ignore[name]) return;
  if (options.onBeforeReplace) options.onBeforeReplace(el, options);
  var replace = !options.textless[name], simple = (options.trim === 'simple');
  var style = CSS.getStyle(attach(el, options)).extend(options);
  // may cause issues if the element contains other elements
  // with larger fontSize, however such cases are rare and can
  // be fixed by using a more specific selector
  if (parseFloat(style.get('fontSize')) === 0) return;
  var font = getFont(el, style), node, type, next, anchor, text, lastElement;
  var isShy = options.softHyphens, anyShy = false, pos, shy, reShy = /\u00ad/g;
  var modifyText = options.modifyText;
  if (!font) return;
  for (node = el.firstChild; node; node = next) {
   type = node.nodeType;
   next = node.nextSibling;
   if (replace && type == 3) {
    if (isShy && el.nodeName.toLowerCase() != TAG_SHY) {
     pos = node.data.indexOf('\u00ad');
     if (pos >= 0) {
      node.splitText(pos);
      next = node.nextSibling;
      next.deleteData(0, 1);
      shy = document.createElement(TAG_SHY);
      shy.appendChild(document.createTextNode('\u00ad'));
      el.insertBefore(shy, next);
      next = shy;
      anyShy = true;
     }
    }
    // Node.normalize() is broken in IE 6, 7, 8
    if (anchor) {
     anchor.appendData(node.data);
     el.removeChild(node);
    }
    else anchor = node;
    if (next) continue;
   }
   if (anchor) {
    text = anchor.data;
    if (!isShy) text = text.replace(reShy, '');
    text = CSS.whiteSpace(text, style, anchor, lastElement, simple);
    // modify text only on the first replace
    if (modifyText) text = modifyText(text, anchor, el, options);
    el.replaceChild(process(font, text, style, options, node, el), anchor);
    anchor = null;
   }
   if (type == 1) {
    if (node.firstChild) {
     if (node.nodeName.toLowerCase() == 'cufon') {
      engines[options.engine](font, null, style, options, node, el);
     }
     else arguments.callee(node, options);
    }
    lastElement = node;
   }
  }
  if (isShy && anyShy) {
   updateShy(el);
   if (!trackingShy) addEvent(window, 'resize', updateShyOnResize);
   trackingShy = true;
  }
  if (options.onAfterReplace) options.onAfterReplace(el, options);
 }

 function updateShy(context) {
  var shys, shy, parent, glue, newGlue, next, prev, i;
  shys = context.getElementsByTagName(TAG_SHY);
  // unfortunately there doesn't seem to be any easy
  // way to avoid having to loop through the shys twice.
  for (i = 0; shy = shys[i]; ++i) {
   shy.className = C_SHY_DISABLED;
   glue = parent = shy.parentNode;
   if (glue.nodeName.toLowerCase() != TAG_GLUE) {
    newGlue = document.createElement(TAG_GLUE);
    newGlue.appendChild(shy.previousSibling);
    parent.insertBefore(newGlue, shy);
    newGlue.appendChild(shy);
   }
   else {
    // get rid of double glue (edge case fix)
    glue = glue.parentNode;
    if (glue.nodeName.toLowerCase() == TAG_GLUE) {
     parent = glue.parentNode;
     while (glue.firstChild) {
      parent.insertBefore(glue.firstChild, glue);
     }
     parent.removeChild(glue);
    }
   }
  }
  for (i = 0; shy = shys[i]; ++i) {
   shy.className = '';
   glue = shy.parentNode;
   parent = glue.parentNode;
   next = glue.nextSibling || parent.nextSibling;
   // make sure we're comparing same types
   prev = (next.nodeName.toLowerCase() == TAG_GLUE) ? glue : shy.previousSibling;
   if (prev.offsetTop >= next.offsetTop) {
    shy.className = C_SHY_DISABLED;
    if (prev.offsetTop < next.offsetTop) {
     // we have an annoying edge case, double the glue
     newGlue = document.createElement(TAG_GLUE);
     parent.insertBefore(newGlue, glue);
     newGlue.appendChild(glue);
     newGlue.appendChild(next);
    }
   }
  }
 }

 function updateShyOnResize() {
  if (ignoreResize) return; // needed for IE
  CSS.addClass(DOM.root(), C_VIEWPORT_RESIZING);
  clearTimeout(shyTimer);
  shyTimer = setTimeout(function() {
   ignoreResize = true;
   CSS.removeClass(DOM.root(), C_VIEWPORT_RESIZING);
   updateShy(document);
   ignoreResize = false;
  }, 100);
 }

 var HAS_BROKEN_REGEXP = ' '.split(/\s+/).length == 0;
 var TAG_GLUE = 'cufonglue';
 var TAG_SHY = 'cufonshy';
 var C_SHY_DISABLED = 'cufon-shy-disabled';
 var C_VIEWPORT_RESIZING = 'cufon-viewport-resizing';

 var sharedStorage = new Storage();
 var hoverHandler = new HoverHandler();
 var replaceHistory = new ReplaceHistory();
 var initialized = false;
 var trackingShy = false;
 var shyTimer;
 var ignoreResize = false;

 var engines = {}, fonts = {}, defaultOptions = {
  autoDetect: false,
  engine: null,
  //fontScale: 1,
  //fontScaling: false,
  forceHitArea: false,
  hover: false,
  hoverables: {
   a: true
  },
  ignore: {
   applet: 1,
   canvas: 1,
   col: 1,
   colgroup: 1,
   head: 1,
   iframe: 1,
   map: 1,
   noscript: 1,
   optgroup: 1,
   option: 1,
   script: 1,
   select: 1,
   style: 1,
   textarea: 1,
   title: 1,
   pre: 1
  },
  modifyText: null,
  onAfterReplace: null,
  onBeforeReplace: null,
  printable: true,
  //rotation: 0,
  //selectable: false,
  selector: (
    window.Sizzle
   || (window.jQuery && function(query) { return jQuery(query); }) // avoid noConflict issues
   || (window.dojo && dojo.query)
   || (window.glow && glow.dom && glow.dom.get)
   || (window.Ext && Ext.query)
   || (window.YAHOO && YAHOO.util && YAHOO.util.Selector && YAHOO.util.Selector.query)
   || (window.$$ && function(query) { return $$(query); })
   || (window.$ && function(query) { return $(query); })
   || (document.querySelectorAll && function(query) { return document.querySelectorAll(query); })
   || elementsByTagName
  ),
  separate: 'words', // 'none' and 'characters' are also accepted
  softHyphens: true,
  textless: {
   dl: 1,
   html: 1,
   ol: 1,
   table: 1,
   tbody: 1,
   thead: 1,
   tfoot: 1,
   tr: 1,
   ul: 1
  },
  textShadow: 'none',
  trim: 'advanced'
 };

 var separators = {
  // The first pattern may cause unicode characters above
  // code point 255 to be removed in Safari 3.0. Luckily enough
  // Safari 3.0 does not include non-breaking spaces in \s, so
  // we can just use a simple alternative pattern.
  words: /\s/.test('\u00a0') ? /[^\S\u00a0]+/ : /\s+/,
  characters: '',
  none: /^/
 };

 api.now = function() {
  DOM.ready();
  return api;
 };

 api.refresh = function() {
  replaceHistory.repeat.apply(replaceHistory, arguments);
  return api;
 };

 api.registerEngine = function(id, engine) {
  if (!engine) return api;
  engines[id] = engine;
  return api.set('engine', id);
 };

 api.registerFont = function(data) {
  if (!data) return api;
  var font = new Font(data), family = font.family;
  if (!fonts[family]) fonts[family] = new FontFamily();
  fonts[family].add(font);
  return api.set('fontFamily', '"' + family + '"');
 };

 api.replace = function(elements, options, ignoreHistory) {
  options = merge(defaultOptions, options);
  if (!options.engine) return api; // there's no browser support so we'll just stop here
  if (!initialized) {
   CSS.addClass(DOM.root(), 'cufon-active cufon-loading');
   CSS.ready(function() {
    // fires before any replace() calls, but it doesn't really matter
    CSS.addClass(CSS.removeClass(DOM.root(), 'cufon-loading'), 'cufon-ready');
   });
   initialized = true;
  }
  if (options.hover) options.forceHitArea = true;
  if (options.autoDetect) delete options.fontFamily;
  if (typeof options.textShadow == 'string') {
   options.textShadow = CSS.textShadow(options.textShadow);
  }
  if (typeof options.color == 'string' && /^-/.test(options.color)) {
   options.textGradient = CSS.gradient(options.color);
  }
  else delete options.textGradient;
  if (!ignoreHistory) replaceHistory.add(elements, arguments);
  if (elements.nodeType || typeof elements == 'string') elements = [ elements ];
  CSS.ready(function() {
   for (var i = 0, l = elements.length; i < l; ++i) {
    var el = elements[i];
    if (typeof el == 'string') api.replace(options.selector(el), options, true);
    else replaceElement(el, options);
   }
  });
  return api;
 };

 api.set = function(option, value) {
  defaultOptions[option] = value;
  return api;
 };

 return api;

})();

Cufon.registerEngine('vml', (function() {

 var ns = document.namespaces;
 if (!ns) return;
 ns.add('cvml', 'urn:schemas-microsoft-com:vml');
 ns = null;

 var check = document.createElement('cvml:shape');
 check.style.behavior = 'url(#default#VML)';
 if (!check.coordsize) return; // VML isn't supported
 check = null;

 var HAS_BROKEN_LINEHEIGHT = (document.documentMode || 0) < 8;

 document.write(('<style type="text/css">' +
  'cufoncanvas{text-indent:0;}' +
  '@media screen{' +
   'cvml\\:shape,cvml\\:rect,cvml\\:fill,cvml\\:shadow{behavior:url(#default#VML);display:block;antialias:true;position:absolute;}' +
   'cufoncanvas{position:absolute;text-align:left;}' +
   'cufon{display:inline-block;position:relative;vertical-align:' +
   (HAS_BROKEN_LINEHEIGHT
    ? 'middle'
    : 'text-bottom') +
   ';}' +
   'cufon cufontext{position:absolute;left:-10000in;font-size:1px;text-align:left;}' +
   'cufonshy.cufon-shy-disabled,.cufon-viewport-resizing cufonshy{display:none;}' +
   'cufonglue{white-space:nowrap;display:inline-block;}' +
   '.cufon-viewport-resizing cufonglue{white-space:normal;}' +
   'a cufon{cursor:pointer}' + // ignore !important here
  '}' +
  '@media print{' +
   'cufon cufoncanvas{display:none;}' +
  '}' +
 '</style>').replace(/;/g, '!important;'));

 function getFontSizeInPixels(el, value) {
  return getSizeInPixels(el, /(?:em|ex|%)$|^[a-z-]+$/i.test(value) ? '1em' : value);
 }

 // Original by Dead Edwards.
 // Combined with getFontSizeInPixels it also works with relative units.
 function getSizeInPixels(el, value) {
  if (!isNaN(value) || /px$/i.test(value)) return parseFloat(value);
  var style = el.style.left, runtimeStyle = el.runtimeStyle.left;
  el.runtimeStyle.left = el.currentStyle.left;
  el.style.left = value.replace('%', 'em');
  var result = el.style.pixelLeft;
  el.style.left = style;
  el.runtimeStyle.left = runtimeStyle;
  return result;
 }

 function getSpacingValue(el, style, size, property) {
  var key = 'computed' + property, value = style[key];
  if (isNaN(value)) {
   value = style.get(property);
   style[key] = value = (value == 'normal') ? 0 : ~~size.convertFrom(getSizeInPixels(el, value));
  }
  return value;
 }

 var fills = {};

 function gradientFill(gradient) {
  var id = gradient.id;
  if (!fills[id]) {
   var stops = gradient.stops, fill = document.createElement('cvml:fill'), colors = [];
   fill.type = 'gradient';
   fill.angle = 180;
   fill.focus = '0';
   fill.method = 'none';
   fill.color = stops[0][1];
   for (var j = 1, k = stops.length - 1; j < k; ++j) {
    colors.push(stops[j][0] * 100 + '% ' + stops[j][1]);
   }
   fill.colors = colors.join(',');
   fill.color2 = stops[k][1];
   fills[id] = fill;
  }
  return fills[id];
 }

 return function(font, text, style, options, node, el, hasNext) {

  var redraw = (text === null);

  if (redraw) text = node.alt;

  var viewBox = font.viewBox;

  var size = style.computedFontSize || (style.computedFontSize = new Cufon.CSS.Size(getFontSizeInPixels(el, style.get('fontSize')) + 'px', font.baseSize));

  var wrapper, canvas;

  if (redraw) {
   wrapper = node;
   canvas = node.firstChild;
  }
  else {
   wrapper = document.createElement('cufon');
   wrapper.className = 'cufon cufon-vml';
   wrapper.alt = text;

   canvas = document.createElement('cufoncanvas');
   wrapper.appendChild(canvas);

   if (options.printable) {
    var print = document.createElement('cufontext');
    print.appendChild(document.createTextNode(text));
    wrapper.appendChild(print);
   }

   // ie6, for some reason, has trouble rendering the last VML element in the document.
   // we can work around this by injecting a dummy element where needed.
   // @todo find a better solution
   if (!hasNext) wrapper.appendChild(document.createElement('cvml:shape'));
  }

  var wStyle = wrapper.style;
  var cStyle = canvas.style;

  var height = size.convert(viewBox.height), roundedHeight = Math.ceil(height);
  var roundingFactor = roundedHeight / height;
  var stretchFactor = roundingFactor * Cufon.CSS.fontStretch(style.get('fontStretch'));
  var minX = viewBox.minX, minY = viewBox.minY;

  cStyle.height = roundedHeight;
  cStyle.top = Math.round(size.convert(minY - font.ascent));
  cStyle.left = Math.round(size.convert(minX));

  wStyle.height = size.convert(font.height) + 'px';

  var color = style.get('color');
  var chars = Cufon.CSS.textTransform(text, style).split('');

  var jumps = font.spacing(chars,
   getSpacingValue(el, style, size, 'letterSpacing'),
   getSpacingValue(el, style, size, 'wordSpacing')
  );

  if (!jumps.length) return null;

  var width = jumps.total;
  var fullWidth = -minX + width + (viewBox.width - jumps[jumps.length - 1]);

  var shapeWidth = size.convert(fullWidth * stretchFactor), roundedShapeWidth = Math.round(shapeWidth);

  var coordSize = fullWidth + ',' + viewBox.height, coordOrigin;
  var stretch = 'r' + coordSize + 'ns';

  var fill = options.textGradient && gradientFill(options.textGradient);

  var glyphs = font.glyphs, offsetX = 0;
  var shadows = options.textShadow;
  var i = -1, j = 0, chr;

  while (chr = chars[++i]) {

   var glyph = glyphs[chars[i]] || font.missingGlyph, shape;
   if (!glyph) continue;

   if (redraw) {
    // some glyphs may be missing so we can't use i
    shape = canvas.childNodes[j];
    while (shape.firstChild) shape.removeChild(shape.firstChild); // shadow, fill
   }
   else {
    shape = document.createElement('cvml:shape');
    canvas.appendChild(shape);
   }

   shape.stroked = 'f';
   shape.coordsize = coordSize;
   shape.coordorigin = coordOrigin = (minX - offsetX) + ',' + minY;
   shape.path = (glyph.d ? 'm' + glyph.d + 'xe' : '') + 'm' + coordOrigin + stretch;
   shape.fillcolor = color;

   if (fill) shape.appendChild(fill.cloneNode(false));

   // it's important to not set top/left or IE8 will grind to a halt
   var sStyle = shape.style;
   sStyle.width = roundedShapeWidth;
   sStyle.height = roundedHeight;

   if (shadows) {
    // due to the limitations of the VML shadow element there
    // can only be two visible shadows. opacity is shared
    // for all shadows.
    var shadow1 = shadows[0], shadow2 = shadows[1];
    var color1 = Cufon.CSS.color(shadow1.color), color2;
    var shadow = document.createElement('cvml:shadow');
    shadow.on = 't';
    shadow.color = color1.color;
    shadow.offset = shadow1.offX + ',' + shadow1.offY;
    if (shadow2) {
     color2 = Cufon.CSS.color(shadow2.color);
     shadow.type = 'double';
     shadow.color2 = color2.color;
     shadow.offset2 = shadow2.offX + ',' + shadow2.offY;
    }
    shadow.opacity = color1.opacity || (color2 && color2.opacity) || 1;
    shape.appendChild(shadow);
   }

   offsetX += jumps[j++];
  }

  // addresses flickering issues on :hover

  var cover = shape.nextSibling, coverFill, vStyle;

  if (options.forceHitArea) {

   if (!cover) {
    cover = document.createElement('cvml:rect');
    cover.stroked = 'f';
    cover.className = 'cufon-vml-cover';
    coverFill = document.createElement('cvml:fill');
    coverFill.opacity = 0;
    cover.appendChild(coverFill);
    canvas.appendChild(cover);
   }

   vStyle = cover.style;

   vStyle.width = roundedShapeWidth;
   vStyle.height = roundedHeight;

  }
  else if (cover) canvas.removeChild(cover);

  wStyle.width = Math.max(Math.ceil(size.convert(width * stretchFactor)), 0);

  if (HAS_BROKEN_LINEHEIGHT) {

   var yAdjust = style.computedYAdjust;

   if (yAdjust === undefined) {
    var lineHeight = style.get('lineHeight');
    if (lineHeight == 'normal') lineHeight = '1em';
    else if (!isNaN(lineHeight)) lineHeight += 'em'; // no unit
    style.computedYAdjust = yAdjust = 0.5 * (getSizeInPixels(el, lineHeight) - parseFloat(wStyle.height));
   }

   if (yAdjust) {
    wStyle.marginTop = Math.ceil(yAdjust) + 'px';
    wStyle.marginBottom = yAdjust + 'px';
   }

  }

  return wrapper;

 };

})());

Cufon.registerEngine('canvas', (function() {

 // Safari 2 doesn't support .apply() on native methods

 var check = document.createElement('canvas');
 if (!check || !check.getContext || !check.getContext.apply) return;
 check = null;

 var HAS_INLINE_BLOCK = Cufon.CSS.supports('display', 'inline-block');

 // Firefox 2 w/ non-strict doctype (almost standards mode)
 var HAS_BROKEN_LINEHEIGHT = !HAS_INLINE_BLOCK && (document.compatMode == 'BackCompat' || /frameset|transitional/i.test(document.doctype.publicId));

 var styleSheet = document.createElement('style');
 styleSheet.type = 'text/css';
 styleSheet.appendChild(document.createTextNode((
  'cufon{text-indent:0;}' +
  '@media screen,projection{' +
   'cufon{display:inline;display:inline-block;position:relative;vertical-align:middle;' +
   (HAS_BROKEN_LINEHEIGHT
    ? ''
    : 'font-size:1px;line-height:1px;') +
   '}cufon cufontext{display:-moz-inline-box;display:inline-block;width:0;height:0;text-align:left;text-indent:-10000in;}' +
   (HAS_INLINE_BLOCK
    ? 'cufon canvas{position:relative;}'
    : 'cufon canvas{position:absolute;}') +
   'cufonshy.cufon-shy-disabled,.cufon-viewport-resizing cufonshy{display:none;}' +
   'cufonglue{white-space:nowrap;display:inline-block;}' +
   '.cufon-viewport-resizing cufonglue{white-space:normal;}' +
  '}' +
  '@media print{' +
   'cufon{padding:0;}' + // Firefox 2
   'cufon canvas{display:none;}' +
  '}'
 ).replace(/;/g, '!important;')));
 document.getElementsByTagName('head')[0].appendChild(styleSheet);

 function generateFromVML(path, context) {
  var atX = 0, atY = 0;
  var code = [], re = /([mrvxe])([^a-z]*)/g, match;
  generate: for (var i = 0; match = re.exec(path); ++i) {
   var c = match[2].split(',');
   switch (match[1]) {
    case 'v':
     code[i] = { m: 'bezierCurveTo', a: [ atX + ~~c[0], atY + ~~c[1], atX + ~~c[2], atY + ~~c[3], atX += ~~c[4], atY += ~~c[5] ] };
     break;
    case 'r':
     code[i] = { m: 'lineTo', a: [ atX += ~~c[0], atY += ~~c[1] ] };
     break;
    case 'm':
     code[i] = { m: 'moveTo', a: [ atX = ~~c[0], atY = ~~c[1] ] };
     break;
    case 'x':
     code[i] = { m: 'closePath' };
     break;
    case 'e':
     break generate;
   }
   context[code[i].m].apply(context, code[i].a);
  }
  return code;
 }

 function interpret(code, context) {
  for (var i = 0, l = code.length; i < l; ++i) {
   var line = code[i];
   context[line.m].apply(context, line.a);
  }
 }

 return function(font, text, style, options, node, el) {

  var redraw = (text === null);

  if (redraw) text = node.getAttribute('alt');

  var viewBox = font.viewBox;

  var size = style.getSize('fontSize', font.baseSize);

  var expandTop = 0, expandRight = 0, expandBottom = 0, expandLeft = 0;
  var shadows = options.textShadow, shadowOffsets = [];
  if (shadows) {
   for (var i = shadows.length; i--;) {
    var shadow = shadows[i];
    var x = size.convertFrom(parseFloat(shadow.offX));
    var y = size.convertFrom(parseFloat(shadow.offY));
    shadowOffsets[i] = [ x, y ];
    if (y < expandTop) expandTop = y;
    if (x > expandRight) expandRight = x;
    if (y > expandBottom) expandBottom = y;
    if (x < expandLeft) expandLeft = x;
   }
  }

  var chars = Cufon.CSS.textTransform(text, style).split('');

  var jumps = font.spacing(chars,
   ~~size.convertFrom(parseFloat(style.get('letterSpacing')) || 0),
   ~~size.convertFrom(parseFloat(style.get('wordSpacing')) || 0)
  );

  if (!jumps.length) return null; // there's nothing to render

  var width = jumps.total;

  expandRight += viewBox.width - jumps[jumps.length - 1];
  expandLeft += viewBox.minX;

  var wrapper, canvas;

  if (redraw) {
   wrapper = node;
   canvas = node.firstChild;
  }
  else {
   wrapper = document.createElement('cufon');
   wrapper.className = 'cufon cufon-canvas';
   wrapper.setAttribute('alt', text);

   canvas = document.createElement('canvas');
   wrapper.appendChild(canvas);

   if (options.printable) {
    var print = document.createElement('cufontext');
    print.appendChild(document.createTextNode(text));
    wrapper.appendChild(print);
   }
  }

  var wStyle = wrapper.style;
  var cStyle = canvas.style;

  var height = size.convert(viewBox.height);
  var roundedHeight = Math.ceil(height);
  var roundingFactor = roundedHeight / height;
  var stretchFactor = roundingFactor * Cufon.CSS.fontStretch(style.get('fontStretch'));
  var stretchedWidth = width * stretchFactor;

  var canvasWidth = Math.ceil(size.convert(stretchedWidth + expandRight - expandLeft));
  var canvasHeight = Math.ceil(size.convert(viewBox.height - expandTop + expandBottom));

  canvas.width = canvasWidth;
  canvas.height = canvasHeight;

  // needed for WebKit and full page zoom
  cStyle.width = canvasWidth + 'px';
  cStyle.height = canvasHeight + 'px';

  // minY has no part in canvas.height
  expandTop += viewBox.minY;

  cStyle.top = Math.round(size.convert(expandTop - font.ascent)) + 'px';
  cStyle.left = Math.round(size.convert(expandLeft)) + 'px';

  var wrapperWidth = Math.max(Math.ceil(size.convert(stretchedWidth)), 0) + 'px';

  if (HAS_INLINE_BLOCK) {
   wStyle.width = wrapperWidth;
   wStyle.height = size.convert(font.height) + 'px';
  }
  else {
   wStyle.paddingLeft = wrapperWidth;
   wStyle.paddingBottom = (size.convert(font.height) - 1) + 'px';
  }

  var g = canvas.getContext('2d'), scale = height / viewBox.height;

  // proper horizontal scaling is performed later
  g.scale(scale, scale * roundingFactor);
  g.translate(-expandLeft, -expandTop);
  g.save();

  function renderText() {
   var glyphs = font.glyphs, glyph, i = -1, j = -1, chr;
   g.scale(stretchFactor, 1);
   while (chr = chars[++i]) {
    var glyph = glyphs[chars[i]] || font.missingGlyph;
    if (!glyph) continue;
    if (glyph.d) {
     g.beginPath();
     if (glyph.code) interpret(glyph.code, g);
     else glyph.code = generateFromVML('m' + glyph.d, g);
     g.fill();
    }
    g.translate(jumps[++j], 0);
   }
   g.restore();
  }

  if (shadows) {
   for (var i = shadows.length; i--;) {
    var shadow = shadows[i];
    g.save();
    g.fillStyle = shadow.color;
    g.translate.apply(g, shadowOffsets[i]);
    renderText();
   }
  }

  var gradient = options.textGradient;
  if (gradient) {
   var stops = gradient.stops, fill = g.createLinearGradient(0, viewBox.minY, 0, viewBox.maxY);
   for (var i = 0, l = stops.length; i < l; ++i) {
    fill.addColorStop.apply(fill, stops[i]);
   }
   g.fillStyle = fill;
  }
  else g.fillStyle = style.get('color');

  renderText();

  return wrapper;

 };

})());
