<commit_before>execute(function($) {
//==================================================[API 参考]
  declareModule('reference', function(listen, notify) {
//--------------------------------------------------[getCategory]
    var getCategory = function(symbol) {
      return symbol ? (symbol.isConstructor ? 'constructor' : (symbol.isStatic ? 'static' : 'instance')) : '';
    };

//--------------------------------------------------[getType]
    var getType = function(symbol) {
      return symbol && symbol.type ? '<' + symbol.type + '>' : '';
    };

//--------------------------------------------------[getSyntax]
    var getSyntax = function(symbol, name) {
      var syntax;
      if (symbol) {
        syntax = '';
        if (!symbol.isStatic) {
          name = name.replace('.prototype.', '.');
        }
        var parents = name.split('.');
        var symbolName = parents.pop();
        // Global 对象不在语法中显示。
        if (parents[0] === 'Global') {
          parents.shift();
        }
        // 列出其他的 namespace 或 instance。
        var lastIndex = parents.length - 1;
        parents.forEach(function(member, index) {
          if (index === lastIndex && !symbol.isStatic) {
            syntax += '<samp>' + member.toLowerCase() + '</samp>.';
          } else {
            syntax += '' + member + '.';
          }
        });
        // 本对象的名称。
        syntax += '<dfn>' + symbolName + '</dfn>';
        // 生成函数对象的参数部分。
        if (symbol.isFunction) {
          var parameters = symbol.parameters;
          var optionalCount = 0;
          syntax += '(' + parameters.filter(
              function(parameter) {
                // 忽略选项中的参数。
                return !parameter.name.contains('.');
              }
          ).map(
              function(parameter, index) {
                var name = '<var>' + parameter.name + '</var>';
                var separator = index ? ', ' : '';
                if (parameter.isOptional) {
                  name = '[' + separator + name;
                  optionalCount++;
                } else {
                  name = separator + name;
                }
                return name;
              }
          ).join('') + ']'.repeat(optionalCount) + ')';
        }
      } else {
        syntax = '<samp>' + name + '</samp> *';
      }
      return syntax;
    };

//--------------------------------------------------[getShortDescription]
    var shortDescriptionPattern = /
(.*?)<\/p>/;
    var getShortDescription = function(symbol) {
      var shortDescription = '-';
      if (symbol) {
        var match = symbol.description.match(shortDescriptionPattern);
        if (match) {
          shortDescription = match[1];
        }
      }
      return shortDescription;
    };

//--------------------------------------------------[getDescription]
    var getDescription = function(symbol) {
      return '
' + (symbol ? symbol.description : '-') + '
';
    };

//--------------------------------------------------[getParameters]
    var objectKeyPattern = /\.(\w+)/;
    var getParameters = function(symbol) {
      return symbol && symbol.parameters.length ?
          '

参数：

' + symbol.parameters.map(
              function(parameter) {
                return '
<' + parameter.type + '>	<var>' + parameter.name.replace(objectKeyPattern, '.<dfn>$1</dfn>') + (parameter.isOptional ? 'Optional' : '') + '</var>	' + parameter.description + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getReturns]
    var getReturns = function(symbol) {
      return symbol && symbol.returns.length ?
          '

返回值：

' + symbol.returns.map(
              function(returnValue) {
                return '
<' + returnValue.type + '>	' + returnValue.description + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getFires]
    var getFires = function(symbol) {
      return symbol && symbol.fires.length ?
          '

触发事件：

' + symbol.fires.map(
              function(event) {
                return '
<dfn>' + event.name + '</dfn>	' + event.description + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getRequires]
    var getRequires = function(symbol) {
      return symbol && symbol.requires.length ?
          '

要求：
' + symbol.requires.map(
              function(require) {
                return '
' + require + '
';
              }
          ).join('') + '
' :
          '';
    };

//--------------------------------------------------[getSince]
    var getSince = function(symbol) {
      return symbol && symbol.since ? '

在以下版本中加入：

' + symbol.since + '

' : '';
    };

//--------------------------------------------------[getDeprecated]
    var getDeprecated = function(symbol) {
      return symbol && symbol.deprecated ? '

已过期：

' + symbol.deprecated + '

' : '';
    };

//--------------------------------------------------[getExample]
    var getExample = function(symbol) {
      return symbol && symbol.examples.length ?
          '

示例：

' + symbol.examples.map(
              function(example) {
                return '
' + example + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getSee]
    var getSee = function(symbol) {
      return symbol && symbol.see.length ?
          '

请参阅：
' + symbol.see.map(
              function(see) {
                return '
' + see + '
';
              }
          ).join('') + '
' :
          '';
    };

//--------------------------------------------------[getAuthor]
//    var getAuthor = function(symbol) {
//      return symbol && symbol.author ? '

Author:

' + symbol.author + '

' : '';
//    };

//--------------------------------------------------[生成一类对象的文档]
    var indexColumns = {
      a: $('#column_a'),
      b: $('#column_b'),
      c: $('#column_c')
    };
    // 同时生成索引文档和细节文档，side 参数仅供索引文档使用。
    var buildDocument = function($container, name, isBuiltIn) {
      // 本类对象的标题。
      var $indexFieldset = $('<fieldset' + (isBuiltIn ? '' : ' class="optional"') + '><legend><dfn>' + name + '</dfn>' + (isBuiltIn ? '' : '(可选)') + '</legend></fieldset>');
      var $detailsDiv = $('

<dfn>' + name + '</dfn>

');
      if (!manifest[name]) {
        return;
      }
      // 本类对象包含的内容分组。
      var group = {
        constructor: '
构造函数
',
        methods: '
方法
',
        properties: '
属性
'
      };
      // 注解信息（可以作用于一类对象内的多个 API）。
      var comment = '';
      var lastGroupName;
      manifest[name].forEach(function(name) {
        if (name.startsWith('#')) {
          comment = name.slice(1);
          $('
' + comment + '
').insertTo($indexFieldset);
        } else {
          var symbol = apiData[name];
          var category = getCategory(symbol);
          // 语法和说明。
          $('

' + getSyntax(symbol, name) + '

' + getShortDescription(symbol) + '

').insertTo($indexFieldset);
          // 详细信息。
          var groupName = symbol ? (symbol.isFunction ? (symbol.isConstructor ? 'constructor' : 'methods') : 'properties') : '';
          if (groupName && groupName !== lastGroupName) {
            $(group[groupName]).insertTo($detailsDiv);
          }
          $('
' + '
' + (comment ? '' + comment + '' : '') + '' + category + '' + getType(symbol) + getSyntax(symbol, name) + '
' + getDescription(symbol) + getParameters(symbol) + getReturns(symbol) + getFires(symbol) + getRequires(symbol) + getSince(symbol) + getDeprecated(symbol) + getExample(symbol) + getSee(symbol) + '
').insertTo($detailsDiv);
          lastGroupName = groupName;
        }
      });
      $detailsDiv.insertTo($('#details'));
      $indexFieldset.insertTo($container);
    };

//--------------------------------------------------[列出名单中的指定内容]
    listen('build', function() {
      $('
JS 原生对象
').insertTo(indexColumns.a);
      [
        'Global',
        'Object',
        'Function',
        'Array',
        'String',
        'Boolean',
        'Number',
        'Math',
        'Date',
        'RegExp',
        'JSON'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.a, name, true);
          });
      $('
浏览器内置对象
').insertTo(indexColumns.a);
      [
        'navigator',
        'location',
        'cookie',
        'localStorage'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.a, name, true);
          });
      $('
DOM 对象
').insertTo(indexColumns.b);
      [
        'window',
        'document',
        'Element',
        'HTMLFormElement',
        'Event'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.b, name, true);
          });
      $('
特性
').insertTo(indexColumns.c);
      [
        'Configurable',
        'Observable'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.c, name, true);
          });
      $('
组件
').insertTo(indexColumns.c);
      [
        'Component',
        'Animation',
        'Request',
        'Switcher'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.c, name, true);
          });
      [
        'TabPanel',
        'Slideshow',
        'Dialog',
        'Paginator',
        'Calendar',
        'Validator'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.c, name, false);
          });
    });

  });

//==================================================[API 细节]
  var $html = $(document.documentElement);
  var $header = $('#header');
  var $content = $('#content');
  var $deatilsPanel = $('#details_container');
  var $detailsClose = $('#details_close');
  var $currentDetails = null;

  declareModule('details', function(listen, notify) {
//--------------------------------------------------[细节层]
    // 获取滚动条宽度。
    var scrollbarWidth = function() {
      var $outer = $('

').setStyles({position: 'absolute', top: 0, left: -10000, width: 100, height: 100, overflow: 'scroll'});
      var $inner = $('

').setStyles({height: 200});
      $inner.insertTo($outer.insertTo(document.body));
      var width = 100 - $inner.offsetWidth;
      $outer.remove();
      return width;
    }();

    // 调整位置。
    var adjustDeatilsPanel = function() {
      var pinnedOffsetX = $content.getClientRect().left + 50;
      var clientSize = window.getClientSize();
      $deatilsPanel.setStyles({
        width: clientSize.width - pinnedOffsetX,
        height: clientSize.height
      });
      $detailsClose.setStyles({
        left: Math.max(710 - 20, clientSize.width - pinnedOffsetX - 55)
      });
      deatilsPanel.setConfig({offsetX: pinnedOffsetX});
    };

    // 使用对话框实现。
    var deatilsPanel = new Dialog($deatilsPanel, {
      overlayStyles: {background: 'black', opacity: .05},
      offsetX: 0,
      offsetY: 0
    })
        .on('open', function() {
          // 按下 ESC 键或点击细节层外即关闭细节层。
          $html.on('keydown.deatilsPanel, mousedown.deatilsPanel', function(e) {
            if (e.isMouseEvent && !$deatilsPanel.contains(e.target) || e.which === 27) {
              detailsLayer.close();
            }
          });
          // 调整窗口尺寸的同时调整细节层的尺寸。
          window.on('resize.deatilsPanel', adjustDeatilsPanel);
        })
        .on('close', function() {
          if (!navigator.isIE6) {
            $header.setStyle('right', 0);
          }
          $html.setStyles({paddingRight: 0, overflow: ''});
          // 取消事件绑定。
          $html.off('keydown.deatilsPanel, mousedown.deatilsPanel');
          window.off('resize.deatilsPanel');
        });

    // 打开/关闭细节层，包裹对话框的方法。
    var detailsLayer = {
      open: function() {
        if (!this.isOpen) {
          this.isOpen = true;
          var offsetY = window.getPageOffset().y;
          if (!navigator.isIE6) {
            $header.setStyle('right', scrollbarWidth);
          }
          $html.setStyles({paddingRight: scrollbarWidth, overflow: 'hidden'});
          adjustDeatilsPanel();
          deatilsPanel.open();
          window.scrollTo(0, offsetY);
          // 打开时的向左移动的效果。
          var detailsPanelLeft = parseInt($deatilsPanel.getStyle('left'), 10);
          $deatilsPanel.setStyles({left: detailsPanelLeft + 30, opacity: 0}).morph({left: detailsPanelLeft, opacity: 1}, {duration: 150, onStart: function() {
            // 显示当前细节内容。
            $currentDetails.addClass('current');
          }});
        }
      },
      close: function() {
        if (this.isOpen) {
          this.isOpen = false;
          // 关闭时的向右移动的效果。
          var detailsPanelLeft = parseInt($deatilsPanel.getStyle('left'), 10);
          $deatilsPanel.morph({left: detailsPanelLeft + 15, opacity: 0}, {transition: 'easeIn', duration: 150, onFinish: function() {
            // 取消当前细节内容。
            $currentDetails.removeClass('current');
            $currentDetails = null;
            var offsetY = window.getPageOffset().y;
            deatilsPanel.close();
            window.scrollTo(0, offsetY);
          }});
        }
      },
      isOpen: false
    };

    // 点击关闭按钮关闭细节层。
    $detailsClose.on('click', function() {
      detailsLayer.close();
      return false;
    });

//--------------------------------------------------[打开细节层]
    listen('show', function($target) {
      $currentDetails = $target.hasClass('details') ? $target : $target.getParent();
      detailsLayer.open();
    });

  });

//==================================================[本页应用]
  runApplication(function(listen, notify) {
    // 输出文档。
    notify('reference.build');

    // 点击 API 条目，进入细节页的对应位置。
    $content.on('click:relay(a)', function() {
      var href = this.href;
      var id = href.slice(href.indexOf('#'));
      var $target = $(id);
      notify('details.show', $target);
      var scrollTop = $deatilsPanel.scrollTop;
      var top = $target.getClientRect().top + scrollTop;
      $deatilsPanel.scrollTop = scrollTop + ((top - 50) - scrollTop);
      $target.getFirstChild().highlight('yellow', 'backgroundColor', {delay: 150, duration: 1000});
      return false;
    });

    // 如果指定了 hash，则直达细节页的对应位置。
//    if (location.hash) {
//      notify('details.show');
//      var $target = document.$(location.hash);
//      if ($target) {
//        $target.scrollIntoView();
//      }
//    }

    // 是否在索引页显示短描述。
    function showShortDescription(show) {
      $content[show ? 'addClass' : 'removeClass']('show_short_description');
    }

    var $shortDescription = $('#short_description').on('change', function() {
      showShortDescription(this.checked);
      localStorage.setItem('showShortDescription', this.checked);
    });

    if (localStorage.getItem('showShortDescription') === 'true') {
      showShortDescription(true);
      $shortDescription.checked = true;
    }

    // 代码高亮。
    prettyPrint();

  });

}, true);
<commit_msg>修复 Chrome 解析 API 参考页面时偶尔会发生错误的问题。<commit_after>execute(function($) {
//==================================================[API 参考]
  declareModule('reference', function(listen, notify) {
//--------------------------------------------------[getCategory]
    var getCategory = function(symbol) {
      return symbol ? (symbol.isConstructor ? 'constructor' : (symbol.isStatic ? 'static' : 'instance')) : '';
    };

//--------------------------------------------------[getType]
    var getType = function(symbol) {
      return symbol && symbol.type ? '<' + symbol.type + '>' : '';
    };

//--------------------------------------------------[getSyntax]
    var getSyntax = function(symbol, name) {
      var syntax;
      if (symbol) {
        syntax = '';
        if (!symbol.isStatic) {
          name = name.replace('.prototype.', '.');
        }
        var parents = name.split('.');
        var symbolName = parents.pop();
        // Global 对象不在语法中显示。
        if (parents[0] === 'Global') {
          parents.shift();
        }
        // 列出其他的 namespace 或 instance。
        var lastIndex = parents.length - 1;
        parents.forEach(function(member, index) {
          if (index === lastIndex && !symbol.isStatic) {
            syntax += '<samp>' + member.toLowerCase() + '</samp>.';
          } else {
            syntax += '' + member + '.';
          }
        });
        // 本对象的名称。
        syntax += '<dfn>' + symbolName + '</dfn>';
        // 生成函数对象的参数部分。
        if (symbol.isFunction) {
          var parameters = symbol.parameters;
          var optionalCount = 0;
          syntax += '(' + parameters.filter(
              function(parameter) {
                // 忽略选项中的参数。
                return !parameter.name.contains('.');
              }
          ).map(
              function(parameter, index) {
                var name = '<var>' + parameter.name + '</var>';
                var separator = index ? ', ' : '';
                if (parameter.isOptional) {
                  name = '[' + separator + name;
                  optionalCount++;
                } else {
                  name = separator + name;
                }
                return name;
              }
          ).join('') + ']'.repeat(optionalCount) + ')';
        }
      } else {
        syntax = '<samp>' + name + '</samp> *';
      }
      return syntax;
    };

//--------------------------------------------------[getShortDescription]
    var shortDescriptionPattern = /
(.*?)<\/p>/;
    var getShortDescription = function(symbol) {
      var shortDescription = '-';
      if (symbol) {
        var match = symbol.description.match(shortDescriptionPattern);
        if (match) {
          shortDescription = match[1];
        }
      }
      return shortDescription;
    };

//--------------------------------------------------[getDescription]
    var getDescription = function(symbol) {
      return '
' + (symbol ? symbol.description : '-') + '
';
    };

//--------------------------------------------------[getParameters]
    var objectKeyPattern = /\.(\w+)/;
    var getParameters = function(symbol) {
      return symbol && symbol.parameters.length ?
          '

参数：

' + symbol.parameters.map(
              function(parameter) {
                return '
<' + parameter.type + '>	<var>' + parameter.name.replace(objectKeyPattern, '.<dfn>$1</dfn>') + (parameter.isOptional ? 'Optional' : '') + '</var>	' + parameter.description + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getReturns]
    var getReturns = function(symbol) {
      return symbol && symbol.returns.length ?
          '

返回值：

' + symbol.returns.map(
              function(returnValue) {
                return '
<' + returnValue.type + '>	' + returnValue.description + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getFires]
    var getFires = function(symbol) {
      return symbol && symbol.fires.length ?
          '

触发事件：

' + symbol.fires.map(
              function(event) {
                return '
<dfn>' + event.name + '</dfn>	' + event.description + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getRequires]
    var getRequires = function(symbol) {
      return symbol && symbol.requires.length ?
          '

要求：
' + symbol.requires.map(
              function(require) {
                return '
' + require + '
';
              }
          ).join('') + '
' :
          '';
    };

//--------------------------------------------------[getSince]
    var getSince = function(symbol) {
      return symbol && symbol.since ? '

在以下版本中加入：

' + symbol.since + '

' : '';
    };

//--------------------------------------------------[getDeprecated]
    var getDeprecated = function(symbol) {
      return symbol && symbol.deprecated ? '

已过期：

' + symbol.deprecated + '

' : '';
    };

//--------------------------------------------------[getExample]
    var getExample = function(symbol) {
      return symbol && symbol.examples.length ?
          '

示例：

' + symbol.examples.map(
              function(example) {
                return '
' + example + '
';
              }
          ).join('') + '

' :
          '';
    };

//--------------------------------------------------[getSee]
    var getSee = function(symbol) {
      return symbol && symbol.see.length ?
          '

请参阅：
' + symbol.see.map(
              function(see) {
                return '
' + see + '
';
              }
          ).join('') + '
' :
          '';
    };

//--------------------------------------------------[getAuthor]
//    var getAuthor = function(symbol) {
//      return symbol && symbol.author ? '

Author:

' + symbol.author + '

' : '';
//    };

//--------------------------------------------------[生成一类对象的文档]
    var indexColumns = {
      a: $('#column_a'),
      b: $('#column_b'),
      c: $('#column_c')
    };
    // 同时生成索引文档和细节文档，side 参数仅供索引文档使用。
    var buildDocument = function($container, name, isBuiltIn) {
      // 本类对象的标题。
      var $indexFieldset = $('<fieldset' + (isBuiltIn ? '' : ' class="optional"') + '><legend><dfn>' + name + '</dfn>' + (isBuiltIn ? '' : '(可选)') + '</legend></fieldset>');
      var $detailsDiv = $('

<dfn>' + name + '</dfn>

');
      if (!manifest[name]) {
        return;
      }
      // 本类对象包含的内容分组。
      var group = {
        constructor: '
构造函数
',
        methods: '
方法
',
        properties: '
属性
'
      };
      // 注解信息（可以作用于一类对象内的多个 API）。
      var comment = '';
      var lastGroupName;
      manifest[name].forEach(function(name) {
        if (name.startsWith('#')) {
          comment = name.slice(1);
          $('
' + comment + '
').insertTo($indexFieldset);
        } else {
          var symbol = apiData[name];
          var category = getCategory(symbol);
          // 语法和说明。
          $('

' + getSyntax(symbol, name) + '

' + getShortDescription(symbol) + '

').insertTo($indexFieldset);
          // 详细信息。
          var groupName = symbol ? (symbol.isFunction ? (symbol.isConstructor ? 'constructor' : 'methods') : 'properties') : '';
          if (groupName && groupName !== lastGroupName) {
            $(group[groupName]).insertTo($detailsDiv);
          }
          $('
' + '
' + (comment ? '' + comment + '' : '') + '' + category + '' + getType(symbol) + getSyntax(symbol, name) + '
' + getDescription(symbol) + getParameters(symbol) + getReturns(symbol) + getFires(symbol) + getRequires(symbol) + getSince(symbol) + getDeprecated(symbol) + getExample(symbol) + getSee(symbol) + '
').insertTo($detailsDiv);
          lastGroupName = groupName;
        }
      });
      $detailsDiv.insertTo($('#details'));
      $indexFieldset.insertTo($container);
    };

//--------------------------------------------------[列出名单中的指定内容]
    listen('build', function() {
      $('
JS 原生对象
').insertTo(indexColumns.a);
      [
        'Global',
        'Object',
        'Function',
        'Array',
        'String',
        'Boolean',
        'Number',
        'Math',
        'Date',
        'RegExp',
        'JSON'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.a, name, true);
          });
      $('
浏览器内置对象
').insertTo(indexColumns.a);
      [
        'navigator',
        'location',
        'cookie',
        'localStorage'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.a, name, true);
          });
      $('
DOM 对象
').insertTo(indexColumns.b);
      [
        'window',
        'document',
        'Element',
        'HTMLFormElement',
        'Event'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.b, name, true);
          });
      $('
特性
').insertTo(indexColumns.c);
      [
        'Configurable',
        'Observable'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.c, name, true);
          });
      $('
组件
').insertTo(indexColumns.c);
      [
        'Component',
        'Animation',
        'Request',
        'Switcher'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.c, name, true);
          });
      [
        'TabPanel',
        'Slideshow',
        'Dialog',
        'Paginator',
        'Calendar',
        'Validator'
      ]
          .forEach(function(name) {
            buildDocument(indexColumns.c, name, false);
          });
    });

  });

//==================================================[API 细节]
  var $html = $(document.documentElement);
  var $header = $('#header');
  var $content = $('#content');
  var $deatilsPanel = $('#details_container').setStyle('position', 'fixed');
  var $detailsClose = $('#details_close');
  var $currentDetails = null;

  declareModule('details', function(listen, notify) {
//--------------------------------------------------[细节层]
    // 获取滚动条宽度。
    var scrollbarWidth = function() {
      var $outer = $('

').setStyles({position: 'absolute', top: 0, left: -10000, width: 100, height: 100, overflow: 'scroll'});
      var $inner = $('

').setStyles({height: 200});
      $inner.insertTo($outer.insertTo(document.body));
      var width = 100 - $inner.offsetWidth;
      $outer.remove();
      return width;
    }();

    // 调整位置。
    var adjustDeatilsPanel = function() {
      var pinnedOffsetX = $content.getClientRect().left + 50;
      var clientSize = window.getClientSize();
      $deatilsPanel.setStyles({
        width: clientSize.width - pinnedOffsetX,
        height: clientSize.height
      });
      $detailsClose.setStyles({
        left: Math.max(710 - 20, clientSize.width - pinnedOffsetX - 55)
      });
      deatilsPanel.setConfig({offsetX: pinnedOffsetX});
    };

    // 使用对话框实现。
    var deatilsPanel = new Dialog($deatilsPanel, {
      overlayStyles: {background: 'black', opacity: .05},
      offsetX: 0,
      offsetY: 0
    })
        .on('open', function() {
          // 按下 ESC 键或点击细节层外即关闭细节层。
          $html.on('keydown.deatilsPanel, mousedown.deatilsPanel', function(e) {
            if (e.isMouseEvent && !$deatilsPanel.contains(e.target) || e.which === 27) {
              detailsLayer.close();
            }
          });
          // 调整窗口尺寸的同时调整细节层的尺寸。
          window.on('resize.deatilsPanel', adjustDeatilsPanel);
        })
        .on('close', function() {
          if (!navigator.isIE6) {
            $header.setStyle('right', 0);
          }
          $html.setStyles({paddingRight: 0, overflow: ''});
          // 取消事件绑定。
          $html.off('keydown.deatilsPanel, mousedown.deatilsPanel');
          window.off('resize.deatilsPanel');
        });

    // 打开/关闭细节层，包裹对话框的方法。
    var detailsLayer = {
      open: function() {
        if (!this.isOpen) {
          this.isOpen = true;
          var offsetY = window.getPageOffset().y;
          if (!navigator.isIE6) {
            $header.setStyle('right', scrollbarWidth);
          }
          $html.setStyles({paddingRight: scrollbarWidth, overflow: 'hidden'});
          adjustDeatilsPanel();
          deatilsPanel.open();
          window.scrollTo(0, offsetY);
          // 打开时的向左移动的效果。
          var detailsPanelLeft = parseInt($deatilsPanel.getStyle('left'), 10);
          $deatilsPanel.setStyles({left: detailsPanelLeft + 30, opacity: 0}).morph({left: detailsPanelLeft, opacity: 1}, {duration: 150, onStart: function() {
            // 显示当前细节内容。
            $currentDetails.addClass('current');
          }});
        }
      },
      close: function() {
        if (this.isOpen) {
          this.isOpen = false;
          // 关闭时的向右移动的效果。
          var detailsPanelLeft = parseInt($deatilsPanel.getStyle('left'), 10);
          $deatilsPanel.morph({left: detailsPanelLeft + 15, opacity: 0}, {transition: 'easeIn', duration: 150, onFinish: function() {
            // 取消当前细节内容。
            $currentDetails.removeClass('current');
            $currentDetails = null;
            var offsetY = window.getPageOffset().y;
            deatilsPanel.close();
            window.scrollTo(0, offsetY);
          }});
        }
      },
      isOpen: false
    };

    // 点击关闭按钮关闭细节层。
    $detailsClose.on('click', function() {
      detailsLayer.close();
      return false;
    });

//--------------------------------------------------[打开细节层]
    listen('show', function($target) {
      $currentDetails = $target.hasClass('details') ? $target : $target.getParent();
      detailsLayer.open();
    });

  });

//==================================================[本页应用]
  runApplication(function(listen, notify) {
    // 输出文档。
    notify('reference.build');

    // 点击 API 条目，进入细节页的对应位置。
    $content.on('click:relay(a)', function() {
      var href = this.href;
      var id = href.slice(href.indexOf('#'));
      var $target = $(id);
      notify('details.show', $target);
      var scrollTop = $deatilsPanel.scrollTop;
      var top = $target.getClientRect().top + scrollTop;
      $deatilsPanel.scrollTop = scrollTop + ((top - 50) - scrollTop);
      $target.getFirstChild().highlight('yellow', 'backgroundColor', {delay: 150, duration: 1000});
      return false;
    });

    // 如果指定了 hash，则直达细节页的对应位置。
//    if (location.hash) {
//      notify('details.show');
//      var $target = document.$(location.hash);
//      if ($target) {
//        $target.scrollIntoView();
//      }
//    }

    // 是否在索引页显示短描述。
    function showShortDescription(show) {
      $content[show ? 'addClass' : 'removeClass']('show_short_description');
    }

    var $shortDescription = $('#short_description').on('change', function() {
      showShortDescription(this.checked);
      localStorage.setItem('showShortDescription', this.checked);
    });

    if (localStorage.getItem('showShortDescription') === 'true') {
      showShortDescription(true);
      $shortDescription.checked = true;
    }

    // 代码高亮。
    prettyPrint();

  });

}, true);
<|endoftext|>


<commit_before>var w = 600;
var h = 400;

void setup() {
  size(w, h);
}

void draw() {
  //fill(255,0,0);
  //ellipse(200,200,10,10);
  //rect(200,205,10,10);
  //fill(255,255,255);
  //text('e',200,210);

  // face
  fill(player.skin[0],player.skin[1],player.skin[2]);
  ellipse(player.x,player.y-10,12+player.face*4,20);

  // arms
  rect(player.x-10,player.y+4,4,8);
  rect(player.x+6,player.y+4,4,8);

  // tshirt
  fill(player.tColor[0],player.tColor[1],player.tColor[2]);
  rect(player.x-6,player.y,12,10);
  rect(player.x-10,player.y,4,4);
  rect(player.x+6,player.y,4,4);

  // hat
  rect(player.x-player.face-6, player.y-25, player.face*2+12, 5);
  fill(0,0,100);
  rect(player.x-player.face-6-3, player.y-21, 3, 2);

  // tshirt logo
  fill(255,255,255);
  text(player.tshirt,player.x-3,player.y+7);

  // eye whites
  fill(255,255,255);
  ellipse(player.x-3-player.face,player.y-11,4,5);
  ellipse(player.x+3+player.face,player.y-11,4,5);

  //pupils
  fill(0,0,0);
  ellipse(player.x-3-player.face,player.y-11,1,1);
  ellipse(player.x+3+player.face,player.y-11,1,1);

  // mouth
  //rect(player.x-3,payer.y-5,6,1);

  //pants
  fill(0,100,0);
  rect(player.x-4,player.y+10,8,4);
  rect(player.x-4,player.y+14,3,4);
  rect(player.x+2, player.y+14,3,4);

  //shoes
  fill(0,0,0);
  rect(player.x-5,player.y+19,5,2);
  rect(player.x+2,player.y+19,5,2);

}
<commit_msg>Update drawgame.js<commit_after>var w = 600;
var h = 400;

void setup() {
  size(w, h);
}

void draw() {
  //fill(255,0,0);
  //ellipse(200,200,10,10);
  //rect(200,205,10,10);
  //fill(255,255,255);
  //text('e',200,210);

  // face
  fill(player.skin[0],player.skin[1],player.skin[2]);
  ellipse(player.x,player.y-10,12+player.face*4,20);

  // arms
  rect(player.x-10,player.y+4,4,8);
  rect(player.x+6,player.y+4,4,8);

  // tshirt
  fill(player.tColor[0],player.tColor[1],player.tColor[2]);
  rect(player.x-6,player.y,12,10);
  rect(player.x-10,player.y,4,4);
  rect(player.x+6,player.y,4,4);

  /*
  // hat
  rect(player.x-player.face-6, player.y-25, player.face*2+12, 5);
  fill(0,0,100);
  rect(player.x-player.face-6-3, player.y-21, 3, 2);
  */

  // tshirt logo
  fill(255,255,255);
  text(player.tshirt,player.x-3,player.y+9);

  // eye whites
  fill(255,255,255);
  ellipse(player.x-3-player.face,player.y-11,4,5);
  ellipse(player.x+3+player.face,player.y-11,4,5);

  //pupils
  fill(0,0,0);
  ellipse(player.x-3-player.face,player.y-11,1,1);
  ellipse(player.x+3+player.face,player.y-11,1,1);

  // mouth
  //rect(player.x-3,payer.y-5,6,1);

  //pants
  fill(0,100,0);
  rect(player.x-4,player.y+10,8,4);
  rect(player.x-4,player.y+14,3,4);
  rect(player.x+2, player.y+14,3,4);

  //shoes
  fill(0,0,0);
  rect(player.x-5,player.y+19,5,2);
  rect(player.x+2,player.y+19,5,2);

}
<|endoftext|>


<commit_before>/**
 * Created by justin on 2014-11-29.
 */

var
    StringStream = require('string-stream'),
    batcher = require('win-common')().batcher;

module.exports = function(config,log) {
  return function (bucketName, provider,promisify) {
    var s = new (function StorageService(){})();
    provider = provider || new AWS.S3({params: {Bucket: bucketName}});

    // technically there is a way to promisfy all in one go, but as can be seen here
    // we do not want all the methods promisified...  (getObject for one...)
    // http://stackoverflow.com/questions/26475486/how-do-i-promisify-the-aws-javascript-sdk
    if(!promisify) {
      provider.deleteBucket = p.promisify(provider.deleteBucket);
      provider.createBucket = p.promisify(provider.createBucket);
      provider.putObject = p.promisify(provider.putObject);
      provider.listBuckets = p.promisify(provider.listBuckets);
      provider.deleteObjects = p.promisify(provider.deleteObjects);
      provider.getSignedUrl = p.promisify(provider.getSignedUrl);
    }

    //provider.getObject = p.promisify(provider.getObject);

    s.destroy = function () {
      return s.emptyBucket().then(function () {
        return provider.deleteBucket().then(function () {
          s._bucketName = null;
        });
      });
    };

    s.emptyBucket = function() {
      return s.ready().then(function () {
        return s.list()
      })
      .then(function (allObjects) {
        log('Deleting:', {
          objectCount: allObjects.length,
          bucketName: bucketName});
        return p.all(batcher.chunk(allObjects, 1000, function (chunk) {
              return provider.deleteObjects({
                Delete: {
                  Objects: _.map(chunk, function (x) {
                    return {Key: x.Key}
                  })
                }
              });
            }
          )
        )
      })
    };

    s.ready = function () {
      if (s._bucketName) {
        return p.resolve(s._bucketName);
      }
      else if (s._async) {
        return s._async;
      }
      else {
        log('Initializing bucket:', bucketName);
        return s._async = provider.createBucket().then(function (data) {
          s._bucketName = bucketName;
          s._async = null;
        });
      }
    };

    s.write = function (params) {
      return s.ready().then(function () {
        var key = params.key || s.genKey(params.suffix);
        return provider.putObject({
          Key: key,
          Body: params.data
        }).then(function () {
          return key;
        });
      });
    };

    s.read = function (key) {
      return s.ready().then(function () {
        return provider.getObject({
          Key: key
        }).createReadStream();
      });
    };

    s.genKey = function (suffix) {
      return new Date().toISOString() + '-' + (suffix || Math.floor(Math.random() * 99999));
    };

    s.listBuckets = function () {
      return provider.listBuckets().then(function (data) {
        return data.Buckets;
      });
    };

    s.readString = function (key) {
      return s.read(key).then(function (fileStream) {
        var deferred = p.defer();
        var ss = new StringStream('');
        fileStream.pipe(ss);
        ss.on('end', function () {
          deferred.resolve(ss.toString());
        });
        fileStream.on('error', function (err) {
          deferred.resolve(err);
        });
        return deferred.promise;
      });
    };

    /**
     *
     * @param prefix Only download files that start with this prefix
     * @param marker Only download files after this file (the same thing as s3 api marker.)
     * @param maxCount Will only return up to this amount (it could exceed it by up to 999 at maximum, but no more.)
     * @returns {*}
     */
    s.list = function (prefix, marker, maxCount) {
      return s.ready().then(function () {
        return listAllObjects(prefix, marker, maxCount);
      });
    };

    /***
     * Write the file to S3 if it doesnt already exist.  If file does exist, this does nothing and resolves.
     * @param filePath
     * @param key
     */
    s.writeFile = function(filePath, key) {
      return s.list(key).then(function(results) {
        if (!results || !results.length) {
          return s.write({
            data: fs.createReadStream(filePath),
            key: key
          });
        }
        return key;
      });
    };

    s.save = function(bucket, key, blob) {
      return s.ready().then(function () {
        return p.resolve().then(function() {
          try {
            return JSON.stringify(blob);
          }
          catch (ex) {
            console.log('Failed to stringify body.');
            console.error(ex);
            throw ex;
          }
        })
        .then(function(body) {
          return provider.putObject({
            Key: key,
            Body: body,
            Bucket: bucket
          })
        })
        .then(function () {
          return 's3://'+bucket+'/'+key;
        })
        .catch(function(err) {
          // TODO: check if error is 4xx and if it is just let it bubble up
          // TODO: else if it is 5xx do an exponential backoff retry.
          console.log('Failed to save a file to S3.  Details below:');
          console.log(blob);
          console.error(err);
          throw err;
        });
      });
    };

    s.writeBlobs = function(blobs) {
      return p.map(blobs, function(blob) {
        return s.write(blob);
      });
    };

    s.readBlobs = function(blobs) {
      return p.map(blobs, function(key) {
        return s.readString(key);
      })
      .then(function(data) {
        return _.map(blobs, function(key, idx) {
          return {
            key: key,
            data: data[idx]
          };
        });
      });
    };

    s.getSignedUrl = function(params) {
      // TODO: return signed URL
      //var params = {Bucket: 'bucket', Key: 'key', };
      var s3Params = {
        Key: params.key
      };
      if (params.bucket) s3Params.Bucket = params.bucket;
      if (params.expiryInSecs) s3Params.Expires = params.expiryInSecs;

      return provider.getSignedUrl('getObject', s3Params);
    };

    var partitionKeyCandidates = 'abcdefghijklmnopqrstuvwxyz0123456789'
    var getRandomInt = function() {
      return Math.round(Math.random()*100)
    }

    var getPartitionKey = function(partitionKeySize) {
      var partitionKey = ''
      for(var i =0; i<partitionKeySize; i++) {
        partitionKey += partitionKeyCandidates[getRandomInt()%partitionKeyCandidates.length]
      }
      return partitionKey
    }

    s.generateKey = function(params) {
      params = params || {partitionKeySize: 0}
      params.partitionKeySize = params.partitionKeySize || 0

      var partitionKey = getPartitionKey(params.partitionKeySize)
      var key = ''
      if (partitionKey) key = partitionKey
      return path.join(key,params.path||'')
    }

    function listAllObjects(prefix, marker, maxCount) {
      var allKeys = [], deferred = p.defer();
      maxCount = maxCount || 100000;

      function listAllKeys(marker, prefix) {
        provider.listObjects({Prefix: prefix, Marker: marker, MaxKeys: maxCount}, function (err, data) {
          if (err) {
            deferred.reject(err);
            return;
          }
          allKeys.push(data.Contents);
          if (data.IsTruncated && _.flatten(allKeys).length < maxCount)
            listAllKeys(data.Contents.slice(-1)[0].Key, prefix);
          else
            deferred.resolve(_.flatten(allKeys));
        });
      }

      listAllKeys(marker, prefix);
      return deferred.promise;
    }

    return s;
  };
}
<commit_msg>bugfix<commit_after>/**
 * Created by justin on 2014-11-29.
 */

var
    StringStream = require('string-stream'),
    batcher = require('win-common')().batcher;

module.exports = function(config,log) {
  return function (bucketName, provider,promisify) {
    var s = new (function StorageService(){})();
    provider = provider || new AWS.S3({params: {Bucket: bucketName}});

    // technically there is a way to promisfy all in one go, but as can be seen here
    // we do not want all the methods promisified...  (getObject for one...)
    // http://stackoverflow.com/questions/26475486/how-do-i-promisify-the-aws-javascript-sdk
    if(!promisify) {
      provider.deleteBucket = p.promisify(provider.deleteBucket);
      provider.createBucket = p.promisify(provider.createBucket);
      provider.putObject = p.promisify(provider.putObject);
      provider.listBuckets = p.promisify(provider.listBuckets);
      provider.deleteObjects = p.promisify(provider.deleteObjects);
      provider.getSignedUrl = p.promisify(provider.getSignedUrl);
    }

    //provider.getObject = p.promisify(provider.getObject);

    s.destroy = function () {
      return s.emptyBucket().then(function () {
        return provider.deleteBucket().then(function () {
          s._bucketName = null;
        });
      });
    };

    s.emptyBucket = function() {
      return s.ready().then(function () {
        return s.list()
      })
      .then(function (allObjects) {
        log('Deleting:', {
          objectCount: allObjects.length,
          bucketName: bucketName});
        return p.all(batcher.chunk(allObjects, 1000, function (chunk) {
              return provider.deleteObjects({
                Delete: {
                  Objects: _.map(chunk, function (x) {
                    return {Key: x.Key}
                  })
                }
              });
            }
          )
        )
      })
    };

    s.ready = function () {
      if (s._bucketName) {
        return p.resolve(s._bucketName);
      }
      else if (s._async) {
        return s._async;
      }
      else {
        log('Initializing bucket:', bucketName);
        return s._async = provider.createBucket().then(function (data) {
          s._bucketName = bucketName;
          s._async = null;
        });
      }
    };

    s.write = function (params) {
      return s.ready().then(function () {
        var key = params.key || s.genKey(params.suffix);
        return provider.putObject({
          Key: key,
          Body: params.data
        }).then(function () {
          return key;
        });
      });
    };

    s.read = function (key) {
      return s.ready().then(function () {
        return provider.getObject({
          Key: key
        }).createReadStream();
      });
    };

    s.genKey = function (suffix) {
      return new Date().toISOString() + '-' + (suffix || Math.floor(Math.random() * 99999));
    };

    s.listBuckets = function () {
      return provider.listBuckets().then(function (data) {
        return data.Buckets;
      });
    };

    s.readString = function (key) {
      return s.read(key).then(function (fileStream) {
        var deferred = p.defer();
        var ss = new StringStream('');
        fileStream.pipe(ss);
        ss.on('end', function () {
          deferred.resolve(ss.toString());
        });
        fileStream.on('error', function (err) {
          deferred.reject(err);
        });
        return deferred.promise;
      });
    };

    /**
     *
     * @param prefix Only download files that start with this prefix
     * @param marker Only download files after this file (the same thing as s3 api marker.)
     * @param maxCount Will only return up to this amount (it could exceed it by up to 999 at maximum, but no more.)
     * @returns {*}
     */
    s.list = function (prefix, marker, maxCount) {
      return s.ready().then(function () {
        return listAllObjects(prefix, marker, maxCount);
      });
    };

    /***
     * Write the file to S3 if it doesnt already exist.  If file does exist, this does nothing and resolves.
     * @param filePath
     * @param key
     */
    s.writeFile = function(filePath, key) {
      return s.list(key).then(function(results) {
        if (!results || !results.length) {
          return s.write({
            data: fs.createReadStream(filePath),
            key: key
          });
        }
        return key;
      });
    };

    s.save = function(bucket, key, blob) {
      return s.ready().then(function () {
        return p.resolve().then(function() {
          try {
            return JSON.stringify(blob);
          }
          catch (ex) {
            console.log('Failed to stringify body.');
            console.error(ex);
            throw ex;
          }
        })
        .then(function(body) {
          return provider.putObject({
            Key: key,
            Body: body,
            Bucket: bucket
          })
        })
        .then(function () {
          return 's3://'+bucket+'/'+key;
        })
        .catch(function(err) {
          // TODO: check if error is 4xx and if it is just let it bubble up
          // TODO: else if it is 5xx do an exponential backoff retry.
          console.log('Failed to save a file to S3.  Details below:');
          console.log(blob);
          console.error(err);
          throw err;
        });
      });
    };

    s.writeBlobs = function(blobs) {
      return p.map(blobs, function(blob) {
        return s.write(blob);
      });
    };

    s.readBlobs = function(blobs) {
      return p.map(blobs, function(key) {
        return s.readString(key);
      })
      .then(function(data) {
        return _.map(blobs, function(key, idx) {
          return {
            key: key,
            data: data[idx]
          };
        });
      });
    };

    s.getSignedUrl = function(params) {
      // TODO: return signed URL
      //var params = {Bucket: 'bucket', Key: 'key', };
      var s3Params = {
        Key: params.key
      };
      if (params.bucket) s3Params.Bucket = params.bucket;
      if (params.expiryInSecs) s3Params.Expires = params.expiryInSecs;

      return provider.getSignedUrl('getObject', s3Params);
    };

    var partitionKeyCandidates = 'abcdefghijklmnopqrstuvwxyz0123456789'
    var getRandomInt = function() {
      return Math.round(Math.random()*100)
    }

    var getPartitionKey = function(partitionKeySize) {
      var partitionKey = ''
      for(var i =0; i<partitionKeySize; i++) {
        partitionKey += partitionKeyCandidates[getRandomInt()%partitionKeyCandidates.length]
      }
      return partitionKey
    }

    s.generateKey = function(params) {
      params = params || {partitionKeySize: 0}
      params.partitionKeySize = params.partitionKeySize || 0

      var partitionKey = getPartitionKey(params.partitionKeySize)
      var key = ''
      if (partitionKey) key = partitionKey
      return path.join(key,params.path||'')
    }

    function listAllObjects(prefix, marker, maxCount) {
      var allKeys = [], deferred = p.defer();
      maxCount = maxCount || 100000;

      function listAllKeys(marker, prefix) {
        provider.listObjects({Prefix: prefix, Marker: marker, MaxKeys: maxCount}, function (err, data) {
          if (err) {
            deferred.reject(err);
            return;
          }
          allKeys.push(data.Contents);
          if (data.IsTruncated && _.flatten(allKeys).length < maxCount)
            listAllKeys(data.Contents.slice(-1)[0].Key, prefix);
          else
            deferred.resolve(_.flatten(allKeys));
        });
      }

      listAllKeys(marker, prefix);
      return deferred.promise;
    }

    return s;
  };
}
<|endoftext|>


<commit_before><commit_msg>testing log upload<commit_after><|endoftext|>


<commit_before>/**
	@file HTML element implementation.
*/
"use strict";

/**
	@class HTMLElement
	@classdesc Fairly simple HTML element implementation, that includes
		necessary properties.

	@property dataType {String} The type of item that this instance contains.
	@property content {String} The string contents of the item.
	@property attributes {Object} Arbitrary collection of attributes.
	@property children {HTMLElement[]} Child HTML elements of this one.
*/
function HTMLElement(dataType, content, attributes, children)
{
	if ( dataType ) this.dataType = dataType;
	if ( content ) this.content = content;
	if ( attributes ) this.attributes = attributes;
	if ( children ) this.children = children;
}

/**
	Represent different types of behaviors that need to be used for the types
	of elements we'll encounter.

	@property Text - Arbitrary text.
	@property Tag - Any ordinary HTML tag.
	@property Comment - Commented out HTML. Essentially a Text element.
*/
HTMLElement.DataTypes = {
	Text : "text",
	Tag : "tag",
	Comment : "comment"
}

/**
	Regular expressions that are needed throughout.

	@property Text {RegExp} Strip out all text outside of HTML tags.
	@property Tag {RegExp} Extract all contents of a tag, including attributes.
	@property AttributeName {RegExp} Get the name of an attribute.
	@property AttributeValye {RegExp} Get the value of an attribute after
	getting rid of its name.
	@property Comment {RegExp} Strip out the contents of a comment.
*/
HTMLElement.RegExp = {
	Text : /[^\<]*/,
	Tag : /^\<[^\>]*\>/,
	AttributeName : /\s*\w+[^"'\s=]/,
	AttributeValue : /^([^="']+|"[^"]*"|'[^']*')/,
	Comment : /^\<!--[^]+?(?=--\>)--\>/
}

/**
	Internal method pretty much only used to make sure that we can have
	an empty element to encompass all top level elements that are parsed
	out.
	@private

	@param buffer {String} Remaining HTML to parse.
	@return The array of elements that were generated.
*/
HTMLElement._fromString = function(buffer)
{
	var elements = [];
	// Determine if this element is going to be a text or tag item.
	while( (buffer = buffer.trim()).length) {
		buffer = this.processBufferIntoElements(buffer, elements);
	}
	return elements;
}

/**
	Method to generate an HTML element.

	@param buffer {String} HTML input to be parsed.
	@return The DOM that was passed in parsed into an object.
*/
HTMLElement.fromString = function(buffer)
{
	var element = new HTMLElement();
	element.children = this._fromString(buffer);
	return element;
}

/**
	Process whatever's left into the array passed in.

	@param buffer {String} Remaining HTML.
	@param elements {HTMLElement[]} Array to be filled with HTMLElements.
	@return The remaining HTML to parse.
*/
HTMLElement.processBufferIntoElements = function(buffer, elements)
{
	if ( buffer.charAt(0) == "<" ) {
		return this.processTagFromBufferIntoElements(buffer, elements);
	}
	else {
		var oneText = buffer.match(HTMLElement.RegExp.Text)[0];
		var element = new HTMLElement(HTMLElement.DataTypes.Text, oneText.trim());
		elements.push(element);
		return buffer.substring(oneText.length);
	}
}

/**
	Process the tag at the beginning of the input string into elements.

	@param buffer {String} Remaining HTML.
	@param elements {HTMLElement[]} Array to be filled with HTMLElements.
	@return The remaining HTML to parse.
*/
HTMLElement.processTagFromBufferIntoElements = function(buffer, elements)
{
	// First check to see if it's a comment before continuing.
	var comment = buffer.match(HTMLElement.RegExp.Comment);
	if ( comment && comment.length ) {
		var commentText = comment[0];
		var innerComment = commentText.substring(4, commentText.length - 3).trim();
		var element = new HTMLElement(HTMLElement.DataTypes.Comment, innerComment);
		elements.push(element);
		return buffer.substring(commentText.length);
	}

	var oneTag = buffer.match(HTMLElement.RegExp.Tag)[0];
	buffer = buffer.substring(oneTag.length);
	var tagText = oneTag.substring(1, oneTag.length - 1).trim();

	var element = new HTMLElement(HTMLElement.DataTypes.Tag);

	// Does the element have attributes?
	var tagSplit = tagText.split(/\s/);
	var tagName = tagSplit[0].trim();
	var possibleAttributeText = tagText.substring(tagName.length).trim();
	if ( possibleAttributeText.length ) {
		element.attributes = {};
		HTMLElement.processAttributesFromString(possibleAttributeText, element.attributes);
	}

	element.content = tagName;
	elements.push(element);
	// Seek an end tag. If there is one, everything inside is a child, as long as
	// other like tags don't start inside. Had to add in an optional backslash,
	// since it's possible for tags to appear in quotes, where the slashes can be
	// escaped.
	var endTagRegExp = new RegExp("\\<(?:\\s)*(?:\\\\)?/(?:\\s)*" + tagName + "(?:\\s)*\\>", "mgi");
	var bufferSearch = new RegExp(endTagRegExp);
	var startTagRegExp = new RegExp("\\<(\\s)*" + tagName + "([^\\>])*\\>", "igm");

	var one;
	while ( one = bufferSearch.exec(buffer) ) {
		var matchText = buffer.substring(0, one.index);

		var openMatches = 0;
		var closeMatches = 0;

		var test;
		while (test = startTagRegExp.exec(matchText) ) {
			++openMatches;
		}
		while (test = endTagRegExp.exec(matchText) ) {
			++closeMatches;
		}

		if ( openMatches == closeMatches ) {
			if ( element.content == "script" || element.content == "style" ) {
				element.children = [{
					dataType : HTMLElement.DataTypes.Text,
					content : matchText.trim()
				}];
			}
			else {
				element.children = HTMLElement._fromString(matchText);
			}
			return buffer.substring(matchText.length + one[0].length);
		}
	}
	return buffer;
}

/**
	Parse out elements from a list of attributes.

	@param attributeString {String} The string that contains all of the attributes.
	@param elementAttributes {Object} The object to put all attributes into.
*/
HTMLElement.processAttributesFromString = function(attributeString, elementAttributes)
{
	while ( attributeString.length ) {
		var attributeName = HTMLElement.RegExp.AttributeName.exec(attributeString);
		if ( attributeName ) {
			attributeString = attributeString.substring(attributeName[0].length).trim();
			if ( attributeString.charAt(0) == "=" ) {
				attributeString = attributeString.substring(1).trim();
				var attributeValue = HTMLElement.RegExp.AttributeValue.exec(attributeString);
				elementAttributes[attributeName[0]] = attributeValue[0].replace(/("|')/g, "");
				attributeString = attributeString.substring(attributeValue[0].length).trim();
			}
			else {
				elementAttributes[attributeName] = null;
			}
		}
		else {
			return;
		}
	}
}

/**
	Get the first element found within this one that has the provided id in its
	attributes object.

	@param id {String} The id to search for.
	@return An HTML element with the provided id.
	@return null if no element is found.
*/
HTMLElement.prototype.getElementById = function(id) {
	if ( this.attributes && this.attributes.id == id ) {
		return this;
	}
	for ( var i in this.children ) {
		var elem = this.children[i].getElementById(id);
		if ( elem ) {
			return elem;
		}
	}
	return null;
}

/**
	Get all elements within this one that have the provided class.

	@param className {String} The class that we're searching for.
	@return An array of elements that have the provided class.
*/
HTMLElement.prototype.getElementsByClassName = function(className) {
	var elements = [];
	var elementQueue = [this];
	while ( elementQueue.length ) {
		var element = elementQueue.shift();
		if ( element.attributes && element.attributes.class ) {
			var classes = element.attributes.class.split(" ");
			if ( classes.indexOf(className) != -1 ) {
				elements.push(element);
			}
		}
		if ( element.children ) {
			elementQueue = elementQueue.concat(element.children);
		}
	}
	return elements;
}

/**
	Get all elements that are tag types that have the provided tag name.

	@param tagName {String} The tag to search for.
	@return an array of elements with the provided tag type.
*/
HTMLElement.prototype.getElementsByTagName = function(tagName) {
	var elements = [];
	var elementQueue = [this];
	while ( elementQueue.length ) {
		var element = elementQueue.shift();
		if ( element.dataType == HTMLElement.DataTypes.Tag && element.content == tagName ) {
			elements.push(element);
		}
		if ( element.children ) {
			elementQueue = elementQueue.concat(element.children);
		}
	}
	return elements;
}

/**
	Get a pretty printed string format of the DOM this object contains.

	@return A 4-space tabbed HTML string.
*/
HTMLElement.prototype.toString = function() {
	return this._toString("", 0);
}

/**
	Recursive method for generating a nice formatted HTML string.

	@return The generated HTML string.
*/
HTMLElement.prototype._toString = function(indent) {
	var toString = indent;
	if ( this.dataType == HTMLElement.DataTypes.Text ) {
		toString += this.content + "\n";
	}
	else if ( this.dataType == HTMLElement.DataTypes.Comment ) {
		toString += "<!--" + this.children[0].content + "-->\n";
	}
	else if ( this.dataType == HTMLElement.DataTypes.Tag ) {
		toString += "<" + this.content;
		if ( this.attributes ) {
			for ( var i in this.attributes ) {
				toString += " " + i;
				if ( this.attributes[i] != null ) {
					toString += "=\"" + this.attributes[i] + "\"";
				}
			}
		}
		toString += ">\n";
		if ( this.children ) {
			for ( var i = 0; i < this.children.length; ++i ) {
				toString += this.children[i]._toString(indent + "    ");
			}
		}
		toString += indent + "</" + this.content + ">\n";
	}
	return toString;
}

module.exports = HTMLElement;
<commit_msg>Solved the test in the last commit.<commit_after>/**
	@file HTML element implementation.
*/
"use strict";

/**
	@class HTMLElement
	@classdesc Fairly simple HTML element implementation, that includes
		necessary properties.

	@property dataType {String} The type of item that this instance contains.
	@property content {String} The string contents of the item.
	@property attributes {Object} Arbitrary collection of attributes.
	@property children {HTMLElement[]} Child HTML elements of this one.
*/
function HTMLElement(dataType, content, attributes, children)
{
	if ( dataType ) this.dataType = dataType;
	if ( content ) this.content = content;
	if ( attributes ) this.attributes = attributes;
	if ( children ) this.children = children;
}

/**
	Represent different types of behaviors that need to be used for the types
	of elements we'll encounter.

	@property Text - Arbitrary text.
	@property Tag - Any ordinary HTML tag.
	@property Comment - Commented out HTML. Essentially a Text element.
*/
HTMLElement.DataTypes = {
	Text : "text",
	Tag : "tag",
	Comment : "comment"
}

/**
	Regular expressions that are needed throughout.

	@property Text {RegExp} Strip out all text outside of HTML tags.
	@property Tag {RegExp} Extract all contents of a tag, including attributes.
	@property AttributeName {RegExp} Get the name of an attribute.
	@property AttributeValye {RegExp} Get the value of an attribute after
	getting rid of its name.
	@property Comment {RegExp} Strip out the contents of a comment.
*/
HTMLElement.RegExp = {
	Text : /[^\<]*/,
	AttributeName : /\s*\w+[^"'\s=]/,
	AttributeValue : /^([^="']+|"[^"]*"|'[^']*')/,
	Comment : /^\<!--[^]+?(?=--\>)--\>/
}

/**
	Internal method pretty much only used to make sure that we can have
	an empty element to encompass all top level elements that are parsed
	out.
	@private

	@param buffer {String} Remaining HTML to parse.
	@return The array of elements that were generated.
*/
HTMLElement._fromString = function(buffer)
{
	var elements = [];
	// Determine if this element is going to be a text or tag item.
	while( (buffer = buffer.trim()).length) {
		buffer = this.processBufferIntoElements(buffer, elements);
	}
	return elements;
}

/**
	Method to generate an HTML element.

	@param buffer {String} HTML input to be parsed.
	@return The DOM that was passed in parsed into an object.
*/
HTMLElement.fromString = function(buffer)
{
	var element = new HTMLElement();
	element.children = this._fromString(buffer);
	return element;
}

/**
	Process whatever's left into the array passed in.

	@param buffer {String} Remaining HTML.
	@param elements {HTMLElement[]} Array to be filled with HTMLElements.
	@return The remaining HTML to parse.
*/
HTMLElement.processBufferIntoElements = function(buffer, elements)
{
	if ( buffer.charAt(0) == "<" ) {
		return this.processTagFromBufferIntoElements(buffer, elements);
	}
	else {
		var oneText = buffer.match(HTMLElement.RegExp.Text)[0];
		var element = new HTMLElement(HTMLElement.DataTypes.Text, oneText.trim());
		elements.push(element);
		return buffer.substring(oneText.length);
	}
}

/**
	Find a complete tag inside a given string. Because regular expressions aren't
	very capable of counting recursive cases (tag imposters inside existing tags),
	they need to be manually parsed.

	@param buffer {String} The string to search in for a tag element.
*/
HTMLElement.findTagInString = function(buffer)
{
	var open = 0;
	var i = 0;
	do {
		if ( buffer[i] == "<" ) {
			++open;
		}
		else if ( buffer[i] == ">" ) {
			--open;
		}
		++i;
	} while ( open != 0 && i < buffer.length );
	return buffer.substring(0, i);
}

/**
	Process the tag at the beginning of the input string into elements.

	@param buffer {String} Remaining HTML.
	@param elements {HTMLElement[]} Array to be filled with HTMLElements.
	@return The remaining HTML to parse.
*/
HTMLElement.processTagFromBufferIntoElements = function(buffer, elements)
{
	// First check to see if it's a comment before continuing.
	var comment = buffer.match(HTMLElement.RegExp.Comment);
	if ( comment && comment.length ) {
		var commentText = comment[0];
		var innerComment = commentText.substring(4, commentText.length - 3).trim();
		var element = new HTMLElement(HTMLElement.DataTypes.Comment, innerComment);
		elements.push(element);
		return buffer.substring(commentText.length);
	}

	var oneTag = HTMLElement.findTagInString(buffer);
	buffer = buffer.substring(oneTag.length);
	var tagText = oneTag.substring(1, oneTag.length - 1).trim();

	var element = new HTMLElement(HTMLElement.DataTypes.Tag);

	// Does the element have attributes?
	var tagSplit = tagText.split(/\s/);
	var tagName = tagSplit[0].trim();
	var possibleAttributeText = tagText.substring(tagName.length).trim();
	if ( possibleAttributeText.length ) {
		element.attributes = {};
		HTMLElement.processAttributesFromString(possibleAttributeText, element.attributes);
	}

	element.content = tagName;
	elements.push(element);
	// Seek an end tag. If there is one, everything inside is a child, as long as
	// other like tags don't start inside. Had to add in an optional backslash,
	// since it's possible for tags to appear in quotes, where the slashes can be
	// escaped.
	var endTagRegExp = new RegExp("\\<(?:\\s)*(?:\\\\)?/(?:\\s)*" + tagName + "(?:\\s)*\\>", "mgi");
	var bufferSearch = new RegExp(endTagRegExp);
	var startTagRegExp = new RegExp("\\<(\\s)*" + tagName + "([^\\>])*\\>", "igm");

	var one;
	while ( one = bufferSearch.exec(buffer) ) {
		var matchText = buffer.substring(0, one.index);

		var openMatches = 0;
		var closeMatches = 0;

		var test;
		while (test = startTagRegExp.exec(matchText) ) {
			++openMatches;
		}
		while (test = endTagRegExp.exec(matchText) ) {
			++closeMatches;
		}

		if ( openMatches == closeMatches ) {
			if ( element.content == "script" || element.content == "style" ) {
				element.children = [{
					dataType : HTMLElement.DataTypes.Text,
					content : matchText.trim()
				}];
			}
			else {
				element.children = HTMLElement._fromString(matchText);
			}
			return buffer.substring(matchText.length + one[0].length);
		}
	}
	return buffer;
}

/**
	Parse out elements from a list of attributes.

	@param attributeString {String} The string that contains all of the attributes.
	@param elementAttributes {Object} The object to put all attributes into.
*/
HTMLElement.processAttributesFromString = function(attributeString, elementAttributes)
{
	while ( attributeString.length ) {
		var attributeName = HTMLElement.RegExp.AttributeName.exec(attributeString);
		if ( attributeName ) {
			attributeString = attributeString.substring(attributeName[0].length).trim();
			if ( attributeString.charAt(0) == "=" ) {
				attributeString = attributeString.substring(1).trim();
				var attributeValue = HTMLElement.RegExp.AttributeValue.exec(attributeString);
				elementAttributes[attributeName[0]] = attributeValue[0].replace(/("|')/g, "");
				attributeString = attributeString.substring(attributeValue[0].length).trim();
			}
			else {
				elementAttributes[attributeName] = null;
			}
		}
		else {
			return;
		}
	}
}

/**
	Get the first element found within this one that has the provided id in its
	attributes object.

	@param id {String} The id to search for.
	@return An HTML element with the provided id.
	@return null if no element is found.
*/
HTMLElement.prototype.getElementById = function(id) {
	if ( this.attributes && this.attributes.id == id ) {
		return this;
	}
	for ( var i in this.children ) {
		var elem = this.children[i].getElementById(id);
		if ( elem ) {
			return elem;
		}
	}
	return null;
}

/**
	Get all elements within this one that have the provided class.

	@param className {String} The class that we're searching for.
	@return An array of elements that have the provided class.
*/
HTMLElement.prototype.getElementsByClassName = function(className) {
	var elements = [];
	var elementQueue = [this];
	while ( elementQueue.length ) {
		var element = elementQueue.shift();
		if ( element.attributes && element.attributes.class ) {
			var classes = element.attributes.class.split(" ");
			if ( classes.indexOf(className) != -1 ) {
				elements.push(element);
			}
		}
		if ( element.children ) {
			elementQueue = elementQueue.concat(element.children);
		}
	}
	return elements;
}

/**
	Get all elements that are tag types that have the provided tag name.

	@param tagName {String} The tag to search for.
	@return an array of elements with the provided tag type.
*/
HTMLElement.prototype.getElementsByTagName = function(tagName) {
	var elements = [];
	var elementQueue = [this];
	while ( elementQueue.length ) {
		var element = elementQueue.shift();
		if ( element.dataType == HTMLElement.DataTypes.Tag && element.content == tagName ) {
			elements.push(element);
		}
		if ( element.children ) {
			elementQueue = elementQueue.concat(element.children);
		}
	}
	return elements;
}

/**
	Get a pretty printed string format of the DOM this object contains.

	@return A 4-space tabbed HTML string.
*/
HTMLElement.prototype.toString = function() {
	return this._toString("", 0);
}

/**
	Recursive method for generating a nice formatted HTML string.

	@return The generated HTML string.
*/
HTMLElement.prototype._toString = function(indent) {
	var toString = indent;
	if ( this.dataType == HTMLElement.DataTypes.Text ) {
		toString += this.content + "\n";
	}
	else if ( this.dataType == HTMLElement.DataTypes.Comment ) {
		toString += "<!--" + this.children[0].content + "-->\n";
	}
	else if ( this.dataType == HTMLElement.DataTypes.Tag ) {
		toString += "<" + this.content;
		if ( this.attributes ) {
			for ( var i in this.attributes ) {
				toString += " " + i;
				if ( this.attributes[i] != null ) {
					toString += "=\"" + this.attributes[i] + "\"";
				}
			}
		}
		toString += ">\n";
		if ( this.children ) {
			for ( var i = 0; i < this.children.length; ++i ) {
				toString += this.children[i]._toString(indent + "    ");
			}
		}
		toString += indent + "</" + this.content + ">\n";
	}
	return toString;
}

module.exports = HTMLElement;
<|endoftext|>


<commit_before>var fs = require('fs');
var util = require('util');

var _ = require('underscore');
var _s = require('underscore.string');
var htmlparser = require('htmlparser');

var helper = require('./helper');
var format = require('./formatter');

// Which type of tags should not be parsed
var SKIP_TYPES = [
	'style',
	'script'
];

function htmlToText(html, options) {
	options = options || {};
	_.defaults(options, {
		wordwrap: 80,
		tables: [],
		preserveNewlines: false,
		uppercaseHeadings: true,
		hideLinkHrefIfSameAsText: false,
		linkHrefBaseUrl: null,
		baseElement: 'body',
		returnDomByDefault: true,
		decodeOptions: {
			isAttributeValue: false,
			strict: false
		},
		longWordSplit: {
			wrapCharacters: [],
			forceWrapOnLimit: false
		}
	});

	var handler = new htmlparser.DefaultHandler(function (error, dom) {

	}, {
		verbose: true,
		ignoreWhitespace: true
	});
	new htmlparser.Parser(handler).parseComplete(html);

	options.lineCharCount = 0;

	var result = '';
	var baseElements = helper.isArray(options.baseElement) ? options.baseElement : [options.baseElement];
	for (var idx = 0; idx < baseElements.length; ++idx) {
		result += walk(filterBody(handler.dom, options, baseElements[idx]), options);
	}
	return _s.strip(result);
}

function filterBody(dom, options, baseElement) {
	var result = null;

	var splitTag = helper.splitCssSearchTag(baseElement);

	function walk(dom) {
		if (result) return;
		_.each(dom, function(elem) {
			if (result) return;
			if (elem.name === splitTag.element) {
				var documentClasses = elem.attribs && elem.attribs.class ? elem.attribs.class.split(" ") : [];
				var documentIds = elem.attribs && elem.attribs.id ? elem.attribs.id.split(" ") : [];

				if ((splitTag.classes.every(function (val) { return documentClasses.indexOf(val) >= 0 })) &&
					(splitTag.ids.every(function (val) { return documentIds.indexOf(val) >= 0 }))) {
					result = [elem];
					return;
				}
			}
			if (elem.children) walk(elem.children);
		});
	}
	walk(dom);
	return options.returnDomByDefault ? result || dom : result;
}

function containsTable(attr, tables) {
	if (tables === true) return true;

	function removePrefix(key) {
		return key.substr(1);
	}
	function checkPrefix(prefix) {
		return function(key) {
			return _s.startsWith(key, prefix);
		};
	}
	function filterByPrefix(tables, prefix) {
		return _(tables).chain()
						.filter(checkPrefix(prefix))
						.map(removePrefix)
						.value();
	}
	var classes = filterByPrefix(tables, '.');
	var ids = filterByPrefix(tables, '#');
	return attr && (_.include(classes, attr['class']) || _.include(ids, attr['id']));
}

function walk(dom, options, result) {
	if (arguments.length < 3) {
		result = '';
	}
	var whiteSpaceRegex = /\S$/;
	_.each(dom, function(elem) {
		switch(elem.type) {
			case 'tag':
				switch(elem.name.toLowerCase()) {
					case 'img':
						result += format.image(elem, options);
						break;
					case 'a':
						// Inline element needs a leading space if `result` currently
						// doesn't end with whitespace
						elem.needsSpace = whiteSpaceRegex.test(result);
						result += format.anchor(elem, walk, options);
						break;
					case 'p':
						result += format.paragraph(elem, walk, options);
						break;
					case 'h1':
					case 'h2':
					case 'h3':
					case 'h4':
					case 'h5':
					case 'h6':
						result += format.heading(elem, walk, options);
						break;
					case 'br':
						result += format.lineBreak(elem, walk, options);
						break;
					case 'hr':
						result += format.horizontalLine(elem, walk, options);
						break;
					case 'ul':
						result += format.unorderedList(elem, walk, options);
						break;
					case 'ol':
						result += format.orderedList(elem, walk, options);
						break;
					case 'pre':
						var newOptions = _(options).clone();
						newOptions.isInPre = true;
						result += format.paragraph(elem, walk, newOptions);
						break;
					case 'table':
						if (containsTable(elem.attribs, options.tables)) {
							result += format.table(elem, walk, options);
							break;
						}
					default:
						result = walk(elem.children || [], options, result);
				}
				break;
			case 'text':
				if (elem.raw !== '\r\n') {
					// Text needs a leading space if `result` currently
					// doesn't end with whitespace
					elem.needsSpace = whiteSpaceRegex.test(result);
					result += format.text(elem, options);
				}
				break;
			default:
				if (!_.include(SKIP_TYPES, elem.type)) {
					result = walk(elem.children || [], options, result);
				}
		}

		options.lineCharCount = result.length - (result.lastIndexOf('\n') + 1);
	});
	return result;
}

exports.fromFile = function(file, options, callback) {
	if (!callback) {
		callback = options;
		options = {};
	}
	fs.readFile(file, 'utf8', function(err, str) {
		var result = htmlToText(str, options);
		return callback(null, result);
	});
};

exports.fromString = function(str, options) {
	return htmlToText(str, options || {});
};
<commit_msg>Use native version of isArray<commit_after>var fs = require('fs');
var util = require('util');

var _ = require('underscore');
var _s = require('underscore.string');
var htmlparser = require('htmlparser');

var helper = require('./helper');
var format = require('./formatter');

// Which type of tags should not be parsed
var SKIP_TYPES = [
	'style',
	'script'
];

function htmlToText(html, options) {
	options = options || {};
	_.defaults(options, {
		wordwrap: 80,
		tables: [],
		preserveNewlines: false,
		uppercaseHeadings: true,
		hideLinkHrefIfSameAsText: false,
		linkHrefBaseUrl: null,
		baseElement: 'body',
		returnDomByDefault: true,
		decodeOptions: {
			isAttributeValue: false,
			strict: false
		},
		longWordSplit: {
			wrapCharacters: [],
			forceWrapOnLimit: false
		}
	});

	var handler = new htmlparser.DefaultHandler(function (error, dom) {

	}, {
		verbose: true,
		ignoreWhitespace: true
	});
	new htmlparser.Parser(handler).parseComplete(html);

	options.lineCharCount = 0;

	var result = '';
	var baseElements = Array.isArray(options.baseElement) ? options.baseElement : [options.baseElement];
	for (var idx = 0; idx < baseElements.length; ++idx) {
		result += walk(filterBody(handler.dom, options, baseElements[idx]), options);
	}
	return _s.strip(result);
}

function filterBody(dom, options, baseElement) {
	var result = null;

	var splitTag = helper.splitCssSearchTag(baseElement);

	function walk(dom) {
		if (result) return;
		_.each(dom, function(elem) {
			if (result) return;
			if (elem.name === splitTag.element) {
				var documentClasses = elem.attribs && elem.attribs.class ? elem.attribs.class.split(" ") : [];
				var documentIds = elem.attribs && elem.attribs.id ? elem.attribs.id.split(" ") : [];

				if ((splitTag.classes.every(function (val) { return documentClasses.indexOf(val) >= 0 })) &&
					(splitTag.ids.every(function (val) { return documentIds.indexOf(val) >= 0 }))) {
					result = [elem];
					return;
				}
			}
			if (elem.children) walk(elem.children);
		});
	}
	walk(dom);
	return options.returnDomByDefault ? result || dom : result;
}

function containsTable(attr, tables) {
	if (tables === true) return true;

	function removePrefix(key) {
		return key.substr(1);
	}
	function checkPrefix(prefix) {
		return function(key) {
			return _s.startsWith(key, prefix);
		};
	}
	function filterByPrefix(tables, prefix) {
		return _(tables).chain()
						.filter(checkPrefix(prefix))
						.map(removePrefix)
						.value();
	}
	var classes = filterByPrefix(tables, '.');
	var ids = filterByPrefix(tables, '#');
	return attr && (_.include(classes, attr['class']) || _.include(ids, attr['id']));
}

function walk(dom, options, result) {
	if (arguments.length < 3) {
		result = '';
	}
	var whiteSpaceRegex = /\S$/;
	_.each(dom, function(elem) {
		switch(elem.type) {
			case 'tag':
				switch(elem.name.toLowerCase()) {
					case 'img':
						result += format.image(elem, options);
						break;
					case 'a':
						// Inline element needs a leading space if `result` currently
						// doesn't end with whitespace
						elem.needsSpace = whiteSpaceRegex.test(result);
						result += format.anchor(elem, walk, options);
						break;
					case 'p':
						result += format.paragraph(elem, walk, options);
						break;
					case 'h1':
					case 'h2':
					case 'h3':
					case 'h4':
					case 'h5':
					case 'h6':
						result += format.heading(elem, walk, options);
						break;
					case 'br':
						result += format.lineBreak(elem, walk, options);
						break;
					case 'hr':
						result += format.horizontalLine(elem, walk, options);
						break;
					case 'ul':
						result += format.unorderedList(elem, walk, options);
						break;
					case 'ol':
						result += format.orderedList(elem, walk, options);
						break;
					case 'pre':
						var newOptions = _(options).clone();
						newOptions.isInPre = true;
						result += format.paragraph(elem, walk, newOptions);
						break;
					case 'table':
						if (containsTable(elem.attribs, options.tables)) {
							result += format.table(elem, walk, options);
							break;
						}
					default:
						result = walk(elem.children || [], options, result);
				}
				break;
			case 'text':
				if (elem.raw !== '\r\n') {
					// Text needs a leading space if `result` currently
					// doesn't end with whitespace
					elem.needsSpace = whiteSpaceRegex.test(result);
					result += format.text(elem, options);
				}
				break;
			default:
				if (!_.include(SKIP_TYPES, elem.type)) {
					result = walk(elem.children || [], options, result);
				}
		}

		options.lineCharCount = result.length - (result.lastIndexOf('\n') + 1);
	});
	return result;
}

exports.fromFile = function(file, options, callback) {
	if (!callback) {
		callback = options;
		options = {};
	}
	fs.readFile(file, 'utf8', function(err, str) {
		var result = htmlToText(str, options);
		return callback(null, result);
	});
};

exports.fromString = function(str, options) {
	return htmlToText(str, options || {});
};
<|endoftext|>


<commit_before>
var http = require('http'),
    https = require('https'),
    fs = require('fs'),
    events = require('events'),
    queryString = require('querystring'),
    readline = require('readline'),
    util = require('util');

require('./ext');

try {
    var inspect = require('eyes').inspector();
} catch (e) {
    var inspect = function (obj) { util.puts(util.inspect(obj).white) }
}

var consoles = [];

this.Console = function (host, port, options) {
    this.host = host;
    this.port = parseInt(port);
    this.options = options;
    this.timeout = this.options.timeout ? 5000 : 0;
    this.path = [];
    this.socket = null;
    this.cookies = {};
    consoles.push(this);
};

this.Console.prototype = new(function () {
    this.initialize = function () {
        var that = this;

        this.welcome();

        this.headers = { 'Accept':'*/*' };

        if (this.options.json) {
            this.headers['Accept'] = 'application/json';
            this.headers['Content-Type'] = 'application/json';
        }

        if (this.options.auth) {
            this.headers['Authorization'] = "Basic " +
                new(Buffer)(this.options.auth.username + ':' + this.options.auth.password).toString('base64');
        }

        this.readline = readline.createInterface(process.stdin, process.stdout);

        this.readline.on('line', function (cmd) {
            that.exec(cmd.trim());
        }).on('close', function () {
            process.stdout.write('\n');
            process.exit(0);
        });

        this.prompt();

        return this;
    };
    this.welcome = function () {
        util.puts("> " + ("http-console " + exports.version).bold,
                 "> Welcome, enter .help if you're lost.",
                 "> Connecting to " + this.host + " on port " + this.port + '.');
        util.print('\n');
    };
    this.request = function (method, path, headers, callback) {
        var request, that = this;

        this.headers['Host'] = this.host;

        for (var k in this.headers) { headers[k] = this.headers[k] }

        method = method.toUpperCase();
        path   = encodeURI(path);

        if (this.options.verbose) {
            util.puts('> ' + (method + ' ' + path).grey);
        }

        this.setCookies(headers);

        request = (this.options.useSSL ? https : http).request({
            host:    that.host,
            port:    that.port,
            method:  method,
            path:    path,
            headers: headers
        }, function (res) {
            var body = "";

            res.setEncoding('utf8');

            if (that.options.rememberCookies) { that.rememberCookies(res.headers) }
            res.on('data', function (chunk) { body += chunk });
            res.on('end',  function ()      { callback(res, body) });
        }).on('error', function (e) {
            util.error(e.toString().red);
            that.prompt();
        });

        return request;
    };
    this.setCookies = function (headers) {
        var that = this, header;
        if ((keys = Object.keys(this.cookies)).length) {
            header = keys.filter(function (k) {
                var options = that.cookies[k].options;
                return (!options.expires || options.expires >= Date.now()) &&
                       (!options.path    || ('/' + that.path.join('/')).match(new(RegExp)('^' + options.path)));
            }).map(function (k) {
                return [k, queryString.escape(that.cookies[k].value) || ''].join('=');
            }).join(', ');
            header && (headers['Cookie'] = header);
        }
    };
    this.exec = function (command) {
        var method, headers = {}, path = this.path, body,
            that = this,
            match, req;

        if (this.pending) {
            req = this.request(this.pending.method, this.pending.path, {
                'Content-Length' : command.length
            }, function (res, body) {
                that.printResponse(res, body, function () {
                    that.prompt();
                });
            });
            req.write(command);
            req.end();

            return this.pending = null;
        } else if (command[0] === '/') {
            if (command === '//') {
                this.path = [];
            } else {
                Array.prototype.push.apply(
                    this.path, command.slice(1).split('/')
                );
            }
        } else if (command === '..') {
            this.path.pop();
        } else if (command[0] === '.') {
            switch (command.slice(1)) {
                case 'h':
                case 'headers':
                    exports.merge(headers, this.headers);
                    this.setCookies(headers);
                    this.printHeaders(headers);
                    break;
                case 'default-headers':
                    this.printHeaders(this.headers);
                    break;
                case 'o':
                case 'options':
                    inspect(this.options);
                    break;
                case 'c':
                case 'cookies':
                    inspect(this.cookies);
                    break;
                case 'help':
                    util.puts(exports.help);
                    break;
                case 'j':
                case 'json':
                    this.headers['Content-Type'] = 'application/json';
                    break;
                case 'exit':
                case 'quit':
                case 'q':
                    process.exit(0);
            }
        } else if (command[0] === '\\') {
            this.exec(command.replace(/^\\/, '.'));
        } else if (match = command.match(/^([a-zA-Z-]+):\s*(.*)/)) {
            if (match[2]) {
                this.headers[match[1]] = match[2];
            } else {
                delete(this.headers[match[1]]);
            }
        } else if (/^(GET|POST|PUT|HEAD|DELETE)/i.test(command)) {
            command = command.split(/\s+/);
            method  = command.shift().toUpperCase();
            path    = this.path.slice(0);

            if (command.length > 0) { path.push(command[0]) }

            path = ('/' + path.join('/')).replace(/\/+/g, '/');

            if (method === 'PUT' || method === 'POST') {
                this.pending = { method: method, path: path };
                this.dataPrompt();
            } else {
                this.request(method, path, {}, function (res, body) {
                    that.printResponse.call(that, res, body, function () {
                        that.prompt();
                    });
                }).end();
            }
            return;
        } else if (command) {
            util.puts(("unknown command '" + command + "'").yellow.bold);
        }
        this.prompt();
    };
    this.printResponse = function (res, body, callback) {
        var status = ('HTTP/' + res.httpVersion +
                      ' '     + res.statusCode  +
                      ' '     + http.STATUS_CODES[res.statusCode]).bold, output;

        if      (res.statusCode >= 500) { status = status.red }
        else if (res.statusCode >= 400) { status = status.yellow }
        else if (res.statusCode >= 300) { status = status.cyan }
        else                            { status = status.green }

        util.puts(status);

        this.printHeaders(res.headers);

        util.print('\n');

        try       { output = JSON.parse(body) }
        catch (_) { output = body.trim() }

        if (typeof(output) === 'string') {
            output.length > 0 && util.print(output.white + '\n');
        } else {
            inspect(output);
        }

        // Make sure the buffer is flushed before
        // we display the prompt.
        if (process.stdout.write('')) {
            callback();
        } else {
            process.stdout.on('drain', function () {
                callback();
            });
        }
    };
    this.prompt = function () {
        var protocol = this.options.useSSL ? 'https://' : 'http://',
            path     = '/' + this.path.join('/'),
            host     = this.host + ':' + this.port,
            arrow    = '> ';

        var length = (protocol + host + path + arrow).length;

        this.readline.setPrompt((protocol + host).grey + path + arrow.grey, length);
        this.readline.prompt();
    };
    this.dataPrompt = function () {
        var prompt = '... ';
        this.readline.setPrompt(prompt.grey, prompt.length);
        this.readline.prompt();
    };
    this.printHeaders = function (headers) {
        Object.keys(headers).forEach(function (k) {
            var key = k.replace(/\b([a-z])/g, function (_, m) {
                return m.toUpperCase();
            }).bold;
            util.puts(key + ': ' + headers[k]);
        });
    };
    this.rememberCookies = function (headers) {
        var that = this;
        var parts, cookie, name, value, keys;

        if ('set-cookie' in headers) {
            headers['set-cookie'].forEach(function (c) {
                parts  = c.split(/; */);
                cookie = parts.shift().match(/^(.+?)=(.*)$/).slice(1);
                name   = cookie[0];
                value  = queryString.unescape(cookie[1]);

                cookie = that.cookies[name] = {
                    value: value,
                    options: {}
                };

                parts.forEach(function (part) {
                    part = part.split('=');
                    cookie.options[part[0]] = part.length > 1 ? part[1] : true;
                });

                if (cookie.options.expires) {
                    cookie.options.expires = new(Date)(cookie.options.expires);
                }
            });
        }
    };
});

this.version = fs.readFileSync(require('path').join(__dirname, '..', 'package.json'))
                 .toString().match(/"version"\s*:\s*"([\d.]+)"/)[1];

this.help = [
    '.h[eaders]  ' +  'show active request headers.'.grey,
    '.o[ptions]  ' +  'show options.'.grey,
    '.c[ookies]  ' +  'show client cookies.'.grey,
    '.j[son]     ' +  'set \'Content-Type\' header to \'application/json\'.'.grey,
    '.help       ' +  'display this message.'.grey,
    '.q[uit]     ' +  'exit console.'.grey
].join('\n');

this.merge = function (target /*, objects... */) {
    var args = Array.prototype.slice.call(arguments, 1);

    args.forEach(function (a) {
        var keys = Object.keys(a);
        for (var i = 0; i < keys.length; i++) {
            target[keys[i]] = a[keys[i]];
        }
    });
    return target;
};

process.on('uncaughtException', function (e) {
    util.puts(e.stack.red);
    consoles[consoles.length - 1].prompt();
});

process.on('exit', function () {
    consoles.forEach(function (c) {
        // TODO: Cleanup
    });
    util.print('\n');
});

<commit_msg>Make verbose output request headers and body<commit_after>
var http = require('http'),
    https = require('https'),
    fs = require('fs'),
    events = require('events'),
    queryString = require('querystring'),
    readline = require('readline'),
    util = require('util');

require('./ext');

try {
    var inspect = require('eyes').inspector();
} catch (e) {
    var inspect = function (obj) { util.puts(util.inspect(obj).white) }
}

var consoles = [];

this.Console = function (host, port, options) {
    this.host = host;
    this.port = parseInt(port);
    this.options = options;
    this.timeout = this.options.timeout ? 5000 : 0;
    this.path = [];
    this.socket = null;
    this.cookies = {};
    consoles.push(this);
};

this.Console.prototype = new(function () {
    this.initialize = function () {
        var that = this;

        this.welcome();

        this.headers = { 'Accept':'*/*' };

        if (this.options.json) {
            this.headers['Accept'] = 'application/json';
            this.headers['Content-Type'] = 'application/json';
        }

        if (this.options.auth) {
            this.headers['Authorization'] = "Basic " +
                new(Buffer)(this.options.auth.username + ':' + this.options.auth.password).toString('base64');
        }

        this.readline = readline.createInterface(process.stdin, process.stdout);

        this.readline.on('line', function (cmd) {
            that.exec(cmd.trim());
        }).on('close', function () {
            process.stdout.write('\n');
            process.exit(0);
        });

        this.prompt();

        return this;
    };
    this.welcome = function () {
        util.puts("> " + ("http-console " + exports.version).bold,
                 "> Welcome, enter .help if you're lost.",
                 "> Connecting to " + this.host + " on port " + this.port + '.');
        util.print('\n');
    };
    this.request = function (method, path, headers, callback) {
        var request, that = this;

        this.headers['Host'] = this.host;

        for (var k in this.headers) { headers[k] = this.headers[k] }

        method = method.toUpperCase();
        path   = encodeURI(path);

        if (this.options.verbose) {
            util.puts('> ' + (method + ' ' + path).grey);
            Object.keys(headers).forEach(function (name) {
                util.puts((name + ': ' + headers[name]).grey);
            });
            util.puts('');
        }

        this.setCookies(headers);

        request = (this.options.useSSL ? https : http).request({
            host:    that.host,
            port:    that.port,
            method:  method,
            path:    path,
            headers: headers
        }, function (res) {
            var body = "";

            res.setEncoding('utf8');

            if (that.options.rememberCookies) { that.rememberCookies(res.headers) }
            res.on('data', function (chunk) { body += chunk });
            res.on('end',  function ()      { callback(res, body) });
        }).on('error', function (e) {
            util.error(e.toString().red);
            that.prompt();
        });

        return request;
    };
    this.setCookies = function (headers) {
        var that = this, header;
        if ((keys = Object.keys(this.cookies)).length) {
            header = keys.filter(function (k) {
                var options = that.cookies[k].options;
                return (!options.expires || options.expires >= Date.now()) &&
                       (!options.path    || ('/' + that.path.join('/')).match(new(RegExp)('^' + options.path)));
            }).map(function (k) {
                return [k, queryString.escape(that.cookies[k].value) || ''].join('=');
            }).join(', ');
            header && (headers['Cookie'] = header);
        }
    };
    this.exec = function (command) {
        var method, headers = {}, path = this.path, body,
            that = this,
            match, req;

        if (this.pending) {
            req = this.request(this.pending.method, this.pending.path, {
                'Content-Length' : command.length
            }, function (res, body) {
                that.printResponse(res, body, function () {
                    that.prompt();
                });
            });

            if (this.options.verbose) {
                util.puts(('' + command).grey);
                util.puts('');
            }

            req.write(command);
            req.end();

            return this.pending = null;
        } else if (command[0] === '/') {
            if (command === '//') {
                this.path = [];
            } else {
                Array.prototype.push.apply(
                    this.path, command.slice(1).split('/')
                );
            }
        } else if (command === '..') {
            this.path.pop();
        } else if (command[0] === '.') {
            switch (command.slice(1)) {
                case 'h':
                case 'headers':
                    exports.merge(headers, this.headers);
                    this.setCookies(headers);
                    this.printHeaders(headers);
                    break;
                case 'default-headers':
                    this.printHeaders(this.headers);
                    break;
                case 'o':
                case 'options':
                    inspect(this.options);
                    break;
                case 'c':
                case 'cookies':
                    inspect(this.cookies);
                    break;
                case 'help':
                    util.puts(exports.help);
                    break;
                case 'j':
                case 'json':
                    this.headers['Content-Type'] = 'application/json';
                    break;
                case 'exit':
                case 'quit':
                case 'q':
                    process.exit(0);
            }
        } else if (command[0] === '\\') {
            this.exec(command.replace(/^\\/, '.'));
        } else if (match = command.match(/^([a-zA-Z-]+):\s*(.*)/)) {
            if (match[2]) {
                this.headers[match[1]] = match[2];
            } else {
                delete(this.headers[match[1]]);
            }
        } else if (/^(GET|POST|PUT|HEAD|DELETE)/i.test(command)) {
            command = command.split(/\s+/);
            method  = command.shift().toUpperCase();
            path    = this.path.slice(0);

            if (command.length > 0) { path.push(command[0]) }

            path = ('/' + path.join('/')).replace(/\/+/g, '/');

            if (method === 'PUT' || method === 'POST') {
                this.pending = { method: method, path: path };
                this.dataPrompt();
            } else {
                this.request(method, path, {}, function (res, body) {
                    that.printResponse.call(that, res, body, function () {
                        that.prompt();
                    });
                }).end();
            }
            return;
        } else if (command) {
            util.puts(("unknown command '" + command + "'").yellow.bold);
        }
        this.prompt();
    };
    this.printResponse = function (res, body, callback) {
        var status = ('HTTP/' + res.httpVersion +
                      ' '     + res.statusCode  +
                      ' '     + http.STATUS_CODES[res.statusCode]).bold, output;

        if      (res.statusCode >= 500) { status = status.red }
        else if (res.statusCode >= 400) { status = status.yellow }
        else if (res.statusCode >= 300) { status = status.cyan }
        else                            { status = status.green }

        util.puts(status);

        this.printHeaders(res.headers);

        util.print('\n');

        try       { output = JSON.parse(body) }
        catch (_) { output = body.trim() }

        if (typeof(output) === 'string') {
            output.length > 0 && util.print(output.white + '\n');
        } else {
            inspect(output);
        }

        // Make sure the buffer is flushed before
        // we display the prompt.
        if (process.stdout.write('')) {
            callback();
        } else {
            process.stdout.on('drain', function () {
                callback();
            });
        }
    };
    this.prompt = function () {
        var protocol = this.options.useSSL ? 'https://' : 'http://',
            path     = '/' + this.path.join('/'),
            host     = this.host + ':' + this.port,
            arrow    = '> ';

        var length = (protocol + host + path + arrow).length;

        this.readline.setPrompt((protocol + host).grey + path + arrow.grey, length);
        this.readline.prompt();
    };
    this.dataPrompt = function () {
        var prompt = '... ';
        this.readline.setPrompt(prompt.grey, prompt.length);
        this.readline.prompt();
    };
    this.printHeaders = function (headers) {
        Object.keys(headers).forEach(function (k) {
            var key = k.replace(/\b([a-z])/g, function (_, m) {
                return m.toUpperCase();
            }).bold;
            util.puts(key + ': ' + headers[k]);
        });
    };
    this.rememberCookies = function (headers) {
        var that = this;
        var parts, cookie, name, value, keys;

        if ('set-cookie' in headers) {
            headers['set-cookie'].forEach(function (c) {
                parts  = c.split(/; */);
                cookie = parts.shift().match(/^(.+?)=(.*)$/).slice(1);
                name   = cookie[0];
                value  = queryString.unescape(cookie[1]);

                cookie = that.cookies[name] = {
                    value: value,
                    options: {}
                };

                parts.forEach(function (part) {
                    part = part.split('=');
                    cookie.options[part[0]] = part.length > 1 ? part[1] : true;
                });

                if (cookie.options.expires) {
                    cookie.options.expires = new(Date)(cookie.options.expires);
                }
            });
        }
    };
});

this.version = fs.readFileSync(require('path').join(__dirname, '..', 'package.json'))
                 .toString().match(/"version"\s*:\s*"([\d.]+)"/)[1];

this.help = [
    '.h[eaders]  ' +  'show active request headers.'.grey,
    '.o[ptions]  ' +  'show options.'.grey,
    '.c[ookies]  ' +  'show client cookies.'.grey,
    '.j[son]     ' +  'set \'Content-Type\' header to \'application/json\'.'.grey,
    '.help       ' +  'display this message.'.grey,
    '.q[uit]     ' +  'exit console.'.grey
].join('\n');

this.merge = function (target /*, objects... */) {
    var args = Array.prototype.slice.call(arguments, 1);

    args.forEach(function (a) {
        var keys = Object.keys(a);
        for (var i = 0; i < keys.length; i++) {
            target[keys[i]] = a[keys[i]];
        }
    });
    return target;
};

process.on('uncaughtException', function (e) {
    util.puts(e.stack.red);
    consoles[consoles.length - 1].prompt();
});

process.on('exit', function () {
    consoles.forEach(function (c) {
        // TODO: Cleanup
    });
    util.print('\n');
});

<|endoftext|>