code:
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.

'use strict';

/*<replacement>*/

var Buffer = require('safe-buffer').Buffer;
/*</replacement>*/

var isEncoding = Buffer.isEncoding || function (encoding) {
  encoding = '' + encoding;
  switch (encoding && encoding.toLowerCase()) {
    case 'hex':case 'utf8':case 'utf-8':case 'ascii':case 'binary':case 'base64':case 'ucs2':case 'ucs-2':case 'utf16le':case 'utf-16le':case 'raw':
      return true;
    default:
      return false;
  }
};

function _normalizeEncoding(enc) {
  if (!enc) return 'utf8';
  var retried;
  while (true) {
    switch (enc) {
      case 'utf8':
      case 'utf-8':
        return 'utf8';
      case 'ucs2':
      case 'ucs-2':
      case 'utf16le':
      case 'utf-16le':
        return 'utf16le';
      case 'latin1':
      case 'binary':
        return 'latin1';
      case 'base64':
      case 'ascii':
      case 'hex':
        return enc;
      default:
        if (retried) return; // undefined
        enc = ('' + enc).toLowerCase();
        retried = true;
    }
  }
};

// Do not cache `Buffer.isEncoding` when checking encoding names as some
// modules monkey-patch it to support additional encodings
function normalizeEncoding(enc) {
  var nenc = _normalizeEncoding(enc);
  if (typeof nenc !== 'string' && (Buffer.isEncoding === isEncoding || !isEncoding(enc))) throw new Error('Unknown encoding: ' + enc);
  return nenc || enc;
}

// StringDecoder provides an interface for efficiently splitting a series of
// buffers into a series of JS strings without breaking apart multi-byte
// characters.
exports.StringDecoder = StringDecoder;
function StringDecoder(encoding) {
  this.encoding = normalizeEncoding(encoding);
  var nb;
  switch (this.encoding) {
    case 'utf16le':
      this.text = utf16Text;
      this.end = utf16End;
      nb = 4;
      break;
    case 'utf8':
      this.fillLast = utf8FillLast;
      nb = 4;
      break;
    case 'base64':
      this.text = base64Text;
      this.end = base64End;
      nb = 3;
      break;
    default:
      this.write = simpleWrite;
      this.end = simpleEnd;
      return;
  }
  this.lastNeed = 0;
  this.lastTotal = 0;
  this.lastChar = Buffer.allocUnsafe(nb);
}

StringDecoder.prototype.write = function (buf) {
  if (buf.length === 0) return '';
  var r;
  var i;
  if (this.lastNeed) {
    r = this.fillLast(buf);
    if (r === undefined) return '';
    i = this.lastNeed;
    this.lastNeed = 0;
  } else {
    i = 0;
  }
  if (i < buf.length) return r ? r + this.text(buf, i) : this.text(buf, i);
  return r || '';
};

StringDecoder.prototype.end = utf8End;

// Returns only complete characters in a Buffer
StringDecoder.prototype.text = utf8Text;

// Attempts to complete a partial non-UTF-8 character using bytes from a Buffer
StringDecoder.prototype.fillLast = function (buf) {
  if (this.lastNeed <= buf.length) {
    buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, this.lastNeed);
    return this.lastChar.toString(this.encoding, 0, this.lastTotal);
  }
  buf.copy(this.lastChar, this.lastTotal - this.lastNeed, 0, buf.length);
  this.lastNeed -= buf.length;
};

// Checks the type of a UTF-8 byte, whether it's ASCII, a leading byte, or a
// continuation byte. If an invalid byte is detected, -2 is returned.
function utf8CheckByte(byte) {
  if (byte <= 0x7F) return 0;else if (byte >> 5 === 0x06) return 2;else if (byte >> 4 === 0x0E) return 3;else if (byte >> 3 === 0x1E) return 4;
  return byte >> 6 === 0x02 ? -1 : -2;
}

// Checks at most 3 bytes at the end of a Buffer in order to detect an
// incomplete multi-byte UTF-8 character. The total number of bytes (2, 3, or 4)
// needed to complete the UTF-8 character (if applicable) are returned.
function utf8CheckIncomplete(self, buf, i) {
  var j = buf.length - 1;
  if (j < i) return 0;
  var nb = utf8CheckByte(buf[j]);
  if (nb >= 0) {
    if (nb > 0) self.lastNeed = nb - 1;
    return nb;
  }
  if (--j < i || nb === -2) return 0;
  nb = utf8CheckByte(buf[j]);
  if (nb >= 0) {
    if (nb > 0) self.lastNeed = nb - 2;
    return nb;
  }
  if (--j < i || nb === -2) return 0;
  nb = utf8CheckByte(buf[j]);
  if (nb >= 0) {
    if (nb > 0) {
      if (nb === 2) nb = 0;else self.lastNeed = nb - 3;
    }
    return nb;
  }
  return 0;
}

// Validates as many continuation bytes for a multi-byte UTF-8 character as
// needed or are available. If we see a non-continuation byte where we expect
// one, we "replace" the validated continuation bytes we've seen so far with
// a single UTF-8 replacement character ('\ufffd'), to match v8's UTF-8 decoding
// behavior. The continuation byte check is included three times in the case
// where all of the continuation bytes for a character exist in the same buffer.
// It is also done this way as a slight performance increase instead of using a
// loop.
function utf8CheckExtraBytes(self, buf, p) {
  if ((buf[0] & 0xC0) !== 0x80) {
    self.lastNeed = 0;
    return '\ufffd';
  }
  if (self.lastNeed > 1 && buf.length > 1) {
    if ((buf[1] & 0xC0) !== 0x80) {
      self.lastNeed = 1;
      return '\ufffd';
    }
    if (self.lastNeed > 2 && buf.length > 2) {
      if ((buf[2] & 0xC0) !== 0x80) {
        self.lastNeed = 2;
        return '\ufffd';
      }
    }
  }
}

// Attempts to complete a multi-byte UTF-8 character using bytes from a Buffer.
function utf8FillLast(buf) {
  var p = this.lastTotal - this.lastNeed;
  var r = utf8CheckExtraBytes(this, buf, p);
  if (r !== undefined) return r;
  if (this.lastNeed <= buf.length) {
    buf.copy(this.lastChar, p, 0, this.lastNeed);
    return this.lastChar.toString(this.encoding, 0, this.lastTotal);
  }
  buf.copy(this.lastChar, p, 0, buf.length);
  this.lastNeed -= buf.length;
}

// Returns all complete UTF-8 characters in a Buffer. If the Buffer ended on a
// partial character, the character's bytes are buffered until the required
// number of bytes are available.
function utf8Text(buf, i) {
  var total = utf8CheckIncomplete(this, buf, i);
  if (!this.lastNeed) return buf.toString('utf8', i);
  this.lastTotal = total;
  var end = buf.length - (total - this.lastNeed);
  buf.copy(this.lastChar, 0, end);
  return buf.toString('utf8', i, end);
}

// For UTF-8, a replacement character is added when ending on a partial
// character.
function utf8End(buf) {
  var r = buf && buf.length ? this.write(buf) : '';
  if (this.lastNeed) return r + '\ufffd';
  return r;
}

// UTF-16LE typically needs two bytes per character, but even if we have an even
// number of bytes available, we need to check if we end on a leading/high
// surrogate. In that case, we need to wait for the next two bytes in order to
// decode the last character properly.
function utf16Text(buf, i) {
  if ((buf.length - i) % 2 === 0) {
    var r = buf.toString('utf16le', i);
    if (r) {
      var c = r.charCodeAt(r.length - 1);
      if (c >= 0xD800 && c <= 0xDBFF) {
        this.lastNeed = 2;
        this.lastTotal = 4;
        this.lastChar[0] = buf[buf.length - 2];
        this.lastChar[1] = buf[buf.length - 1];
        return r.slice(0, -1);
      }
    }
    return r;
  }
  this.lastNeed = 1;
  this.lastTotal = 2;
  this.lastChar[0] = buf[buf.length - 1];
  return buf.toString('utf16le', i, buf.length - 1);
}

// For UTF-16LE we do not explicitly append special replacement characters if we
// end on a partial character, we simply let v8 handle that.
function utf16End(buf) {
  var r = buf && buf.length ? this.write(buf) : '';
  if (this.lastNeed) {
    var end = this.lastTotal - this.lastNeed;
    return r + this.lastChar.toString('utf16le', 0, end);
  }
  return r;
}

function base64Text(buf, i) {
  var n = (buf.length - i) % 3;
  if (n === 0) return buf.toString('base64', i);
  this.lastNeed = 3 - n;
  this.lastTotal = 3;
  if (n === 1) {
    this.lastChar[0] = buf[buf.length - 1];
  } else {
    this.lastChar[0] = buf[buf.length - 2];
    this.lastChar[1] = buf[buf.length - 1];
  }
  return buf.toString('base64', i, buf.length - n);
}

function base64End(buf) {
  var r = buf && buf.length ? this.write(buf) : '';
  if (this.lastNeed) return r + this.lastChar.toString('base64', 0, 3 - this.lastNeed);
  return r;
}

// Pass bytes on through for single-byte encodings (e.g. ascii, latin1, hex)
function simpleWrite(buf) {
  return buf.toString(this.encoding);
}

function simpleEnd(buf) {
  return buf && buf.length ? this.write(buf) : '';
}


description: The string_decoder module from Node core

test: tap test/parallel/*.js && node test/verify-dependencies

ci: tap test/parallel/*.js test/ours/*.js --tap | tee test.tap && node test/verify-dependencies.js

type: git

keywords: string decoder browser browserify

license: MIT

license: mit


hpack.js

description: HPACK implementation

test: mocha test/*-test.js

keywords: HPACK HTTP2 compress decompress headers

author: Fedor Indutny <fedor@indutny.com>


code:
var assert = require('assert');
var hpack = require('../');
var fixtures = require('./fixtures');

describe('hpack/compressor', function() {
  var comp;

  beforeEach(function() {
    comp = hpack.compressor.create({
      table: {
        maxSize: 1024
      }
    });
  });

  function expect(arr, enc) {
    function isNumber(num) {
      return typeof num === 'number';
    }

    var out = comp.read().toString('hex');
    if (Array.isArray(arr) && !arr.every(isNumber)) {
      arr = arr.map(function(item) {
        return new Buffer(item, enc);
      });
      arr = Buffer.concat(arr);
    } else {
      arr = new Buffer(arr, enc);
    }
    var actual = arr.toString('hex');
    assert.equal(out, actual);
  }

  describe('indexed field', function() {
    it('should lookup entry from static table', function() {
      comp.write([{ name: ':method', value: 'GET' }]);
      expect([ 0b10000000 | 2 ]);
    });

    it('should fetch entry from the end of the static table', function() {
      comp.write([{ name: 'www-authenticate', value: '' }]);
      expect([ 0b10000000 | 61 ]);
    });
  });

  describe('literal field', function() {
    it('should lookup name in the table (incremental)', function() {
      comp.write([{ name: 'host', value: 'localhost' }]);
      expect('6686a0e41d139d09', 'hex');

      comp.write([{ name: 'host', value: 'localhost' }]);
      expect([ 0b10000000 | 62 ]);
    });

    it('should lookup name in the table (not-incremental)', function() {
      comp.write([{ name: 'host', value: 'localhost', incremental: false }]);
      expect('0f1786a0e41d139d09', 'hex');

      // Should not use the table
      comp.write([{ name: 'host', value: 'localhost' }]);
      expect('6686a0e41d139d09', 'hex');
    });

    it('should evict header field from the table', function() {
      for (var i = 0; i < 1000; i++) {
        comp.write([{ name: 'host', value: 'localhost' + i }]);
        comp.read();
      }

      assert(comp._table.size < comp._table.maxSize);
      assert.equal(comp._table.dynamic.length, 21);
    });
  });

  describe('update size', function() {
    it('should evict header field from the table', function() {
      comp.write([{ name: 'host', value: 'localhost' }]);
      expect('6686a0e41d139d09', 'hex');

      comp.reset();

      // update=0, update=maxSize
      expect('203fe107', 'hex');

      comp.write([{ name: 'host', value: 'localhost' }]);
      expect('6686a0e41d139d09', 'hex');
    });

    it('should send dynamic update if size >= protocolMaxSize', function() {
      comp.updateTableSize(Infinity);

      // update=maxSize
      expect('3fe107', 'hex');
    });
  });

  describe('spec examples', function() {
    beforeEach(function() {
      comp = hpack.compressor.create({
        table: {
          maxSize: 256
        }
      });
    });

    var tests = fixtures.specExamples;

    tests.forEach(function(test, i) {
      var prev = tests[i - 1];
      it('should give expected output on ' + test.id, function() {
        var startFrom = test.continuation ? prev.comp : comp;
        if (!startFrom)
          throw new Error('Previous test failed');
        comp = startFrom;

        comp.write(test.output.map(function(pair) {
          return { name: pair[0], value: pair[1], huffman: test.huffman };
        }));
        expect(test.input.replace(/ /g, ''), 'hex');

        // Verify table contents
        assert.deepEqual(comp._table.dynamic.map(function(header) {
          return [ header.name, header.value, header.totalSize ];
        }).reverse(), test.table);

        // Verify table size
        var expectedSize = test.table.reduce(function(acc, item) {
          return acc + item[2];
        }, 0);
        assert.equal(comp._table.size, expectedSize);

        test.comp = comp;
      });
    });
  });
});


code:
var assert = require('assert');
var hpack = require('../');

describe('hpack/decoder', function() {
  var decoder;

  beforeEach(function() {
    decoder = hpack.decoder.create();
  });

  describe('bit', function() {
    it('should decode number bit-by-bit', function() {
      decoder.push([ 0b11101010, 0b10101111 ]);
      var out = '';
      for (var i = 0; i < 16; i++)
        out += decoder.decodeBit();
      assert.equal(out, '11101010' + '10101111');
    });
  });

  describe('integer', function() {
    it('should decode 10 in prefix-5 (C.1.1)', function() {
      decoder.push([ 0b11101010 ]);
      decoder.skipBits(3);
      assert.equal(decoder.decodeInt(), 10);
    });

    it('should decode 1337 in prefix-5 (C.1.2)', function() {
      decoder.push([ 0b11111111, 0b10011010, 0b00001010 ]);
      decoder.skipBits(3);
      assert.equal(decoder.decodeInt(), 1337);
    });

    it('should decode 42 at octect boundary (C.1.3)', function() {
      decoder.push([ 0b00101010 ]);
      assert.equal(decoder.decodeInt(8), 42);
    });

    it('should throw on empty input', function() {
      assert.throws(function() {
        decoder.decodeInt();
      });
    });

    it('should throw on incomplete int', function() {
      decoder.push([ 0b11111111, 0b10011010 ]);
      decoder.skipBits(3);
      assert.throws(function() {
        decoder.decodeInt();
      });
    });

    it('should throw on overflowing int', function() {
      decoder.push([
        0b11111111,
        0b10011010,
        0b10011010,
        0b10011010,
        0b10011010,
        0b10011010
      ]);
      decoder.skipBits(3);
      assert.throws(function() {
        decoder.decodeInt();
      });
    });
  });

  describe('string', function() {
    it('should decode literal from (C.2.1)', function() {
      decoder.push([ 0x0a ]);
      decoder.push(new Buffer('custom-key'));

      assert.equal(decoder.decodeStr().toString(), 'custom-key');
    });

    it('should decode literal from (C.4.1)', function() {
      decoder.push(new Buffer(
        '8c' +
            'f1e3 c2e5 f23a 6ba0 ab90 f4ff'.replace(/ /g, ''),
        'hex'));

      assert.equal(new Buffer(decoder.decodeStr()).toString(),
                   'www.example.com');
    });

    it('should decode literal from (C.4.2)', function() {
      decoder.push(new Buffer(
        '86' +
            'a8eb 1064 9cbf'.replace(/ /g, ''),
        'hex'));

      assert.equal(new Buffer(decoder.decodeStr()).toString(), 'no-cache');
    });

    it('should decode literal from (C.4.3) #1', function() {
      decoder.push(new Buffer(
        '88' +
            '25a8 49e9 5ba9 7d7f'.replace(/ /g, ''),
        'hex'));

      assert.equal(new Buffer(decoder.decodeStr()).toString(), 'custom-key');
    });

    it('should decode literal from (C.4.3) #2', function() {
      decoder.push(new Buffer(
        '89' +
            '25a8 49e9 5bb8 e8b4 bf'.replace(/ /g, ''),
        'hex'));

      assert.equal(new Buffer(decoder.decodeStr()).toString(), 'custom-value');
    });

    it('should decode literal from (C.6.1) #1', function() {
      decoder.push(new Buffer(
        ('96' +
            'd07a be94 1054 d444 a820 0595 040b 8166' +
            'e082 a62d 1bff').replace(/ /g, ''),
        'hex'));

      assert.equal(new Buffer(decoder.decodeStr()).toString(),
                   'Mon, 21 Oct 2013 20:13:21 GMT');
    });

    it('should decode literal from (C.6.1) #2', function() {
      decoder.push(new Buffer(
        ('91' +
            '9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43' +
            'd3').replace(/ /g, ''),
        'hex'));
      assert.equal(new Buffer(decoder.decodeStr()).toString(),
                   'https://www.example.com');
    });

    it('should decode many 5 bit chars', function() {
      // e = 00101, 0x294A5294A5 = 00101 x 8
      decoder.push(new Buffer('85294A5294A5', 'hex'));
      assert.equal(new Buffer(decoder.decodeStr()).toString(), 'eeeeeeee');
    });

    it('should decode many 5 bit chars with 3-bit EOS', function() {
      // e = 00101, EOS=111,
      // 0x294A5294A52F = 00101 x 9 + 111
      decoder.push(new Buffer('86294A5294A52F', 'hex'));
      assert.equal(new Buffer(decoder.decodeStr()).toString(), 'eeeeeeeee');
    });

    it('should decode many 5 bit chars with 2-bit EOS', function() {
      // e = 00101, EOS=11,
      // 0x294A5297 = 00101 x 6 + 11
      decoder.push(new Buffer('84294A5297', 'hex'));
      assert.equal(new Buffer(decoder.decodeStr()).toString(), 'eeeeee');
    });

    it('should decode many multi-octet chars', function() {
      decoder.push(new Buffer(
          '97ffffb1ffff63fffec7fffd8ffffb1ffff63fffec7fffd8',
          'hex'));
      assert.deepEqual(decoder.decodeStr(), [
        1, 1, 1, 1, 1, 1, 1, 1
      ]);
    });

    it('should fail on 8 bit EOS', function() {
      // e = 00101, 0x294A5294A5 = 00101 x 8
      decoder.push(new Buffer('86294A5294A5ff', 'hex'));
      assert.throws(function() {
        decoder.decodeStr();
      });
    });

    it('should fail on invalid 2-bit EOS', function() {
      // e = 00101, EOS=10,
      // 0x294A5297 = 00101 x 6 + 11
      decoder.push(new Buffer('84294A5296', 'hex'));
      assert.throws(function() {
        decoder.decodeStr();
      });
    });
  });
});


code:
var assert = require('assert');
var hpack = require('../');
var fixtures = require('./fixtures');

describe('hpack/decompressor', function() {
  var decomp;

  beforeEach(function() {
    decomp = hpack.decompressor.create({
      table: {
        maxSize: 1024
      }
    });
  });

  describe('indexed field', function() {
    it('should fail on 0-index', function(cb) {
      decomp.write(new Buffer([ 0b10000000 ]));
      decomp.execute(function(err) {
        assert(/zero index/i.test(err.message), err.message);
        cb();
      });
    });

    it('should fetch entry from static table', function() {
      decomp.write(new Buffer([ 0b10000000 | 2 ]));
      decomp.execute();
      var field = decomp.read();
      assert.equal(field.name, ':method');
      assert.equal(field.value, 'GET');
    });

    it('should fetch entry from the end of the static table', function() {
      decomp.write(new Buffer([ 0b10000000 | 61 ]));
      decomp.execute();
      var field = decomp.read();
      assert.equal(field.name, 'www-authenticate');
      assert.equal(field.value, '');
    });

    it('should fail on OOB-index', function(cb) {
      decomp.write(new Buffer([ 0b11000000 ]));
      decomp.execute(function(err) {
        assert(/field oob/i.test(err.message), err.message);
        cb();
      });
    });
  });

  describe('literal field', function() {
    it('should lookup name in the table (incremental)', function() {
      var value = new Buffer('localhost');
      var header = new Buffer([
        0b01000000 | 38,  // 38th element from static table
        value.length
      ]);
      decomp.write(Buffer.concat([ header, value ]));
      decomp.execute();

      var field = decomp.read();
      assert.equal(field.name, 'host');
      assert.equal(field.value, 'localhost');

      decomp.write(new Buffer([ 0b10000000 | 62 ]));
      decomp.execute();
      var field = decomp.read();
      assert.equal(field.name, 'host');
      assert.equal(field.value, 'localhost');
    });

    it('should lookup name in the table (not-incremental)', function(cb) {
      var value = new Buffer('localhost');
      var header = new Buffer([
        0b00001111,
        0b00000000 | 23,
        value.length
      ]);
      decomp.write(Buffer.concat([ header, value ]));
      decomp.execute();

      var field = decomp.read();
      assert.equal(field.name, 'host');
      assert.equal(field.value, 'localhost');

      decomp.write(new Buffer([ 0b10000000 | 62 ]));
      decomp.execute(function(err) {
        assert(/field oob/i.test(err.message), err.message);
        cb();
      });
    });

    it('should evict header field from the table', function() {
      var value = new Buffer('localhost');
      var header = new Buffer([
        0b01000000 | 38,  // 38th element from static table
        value.length
      ]);
      for (var i = 0; i < 1000; i++) {
        decomp.write(Buffer.concat([ header, value ]));
        decomp.execute();
        var field = decomp.read();
        assert.equal(field.name, 'host');
        assert.equal(field.value, 'localhost');
      }

      assert(decomp._table.size < decomp._table.maxSize);
      assert.equal(decomp._table.dynamic.length, 22);
    });
  });

  describe('update size', function() {
    it('should evict header field from the table', function() {
      var value = new Buffer('localhost');
      var header = new Buffer([
        0b01000000 | 38,  // 38th element from static table
        value.length
      ]);

      decomp.write(Buffer.concat([ header, value ]));
      decomp.execute();
      var field = decomp.read();
      assert.equal(field.name, 'host');
      assert.equal(field.value, 'localhost');
      assert.equal(decomp._table.dynamic.length, 1);

      decomp.write(new Buffer([
        0b00100000
      ]));
      decomp.execute();

      assert.equal(decomp._table.dynamic.length, 0);
    });
  });

  describe('spec examples', function() {
    var decomp;
    beforeEach(function() {
      decomp = hpack.decompressor.create({
        table: {
          maxSize: 256
        }
      });
    });

    var tests = fixtures.specExamples;

    tests.forEach(function(test, i) {
      var prev = tests[i - 1];
      it('should give expected output on ' + test.id, function() {
        var startFrom = test.continuation ? prev.decomp : decomp;
        if (!startFrom)
          throw new Error('Previous test failed');
        decomp = startFrom;

        decomp.write(new Buffer(test.input.replace(/ /g, ''), 'hex'));
        decomp.execute();

        var output = [];
        for (;;) {
          var chunk = decomp.read();
          if (!chunk)
            break;

          output.push([ chunk.name, chunk.value ]);
        }

        assert.deepEqual(output, test.output);

        // Verify table contents
        assert.deepEqual(decomp._table.dynamic.map(function(header) {
          return [ header.name, header.value, header.totalSize ];
        }).reverse(), test.table);

        // Verify table size
        var expectedSize = test.table.reduce(function(acc, item) {
          return acc + item[2];
        }, 0);
        assert.equal(decomp._table.size, expectedSize);

        test.decomp = decomp;
      });
    });
  });
});


code:
var assert = require('assert');
var hpack = require('../');

describe('hpack/encoder', function() {
  var encoder;

  beforeEach(function() {
    encoder = hpack.encoder.create();
  });

  function expect(arr, enc) {
    function isNumber(num) {
      return typeof num === 'number';
    }

    var out = Buffer.concat(encoder.render()).toString('hex');
    if (Array.isArray(arr) && !arr.every(isNumber)) {
      arr = arr.map(function(item) {
        return new Buffer(item, enc);
      });
      arr = Buffer.concat(arr);
    } else {
      arr = new Buffer(arr, enc);
    }
    var actual = arr.toString('hex');
    assert.equal(out, actual);
  }

  describe('bit', function() {
    it('should encode number bit-by-bit', function() {
      [ 1, 1, 1, 0, 1, 0, 1, 0,
        1, 0, 1, 0, 1, 1, 1, 1 ].forEach(function(bit) {
        encoder.encodeBit(bit);
      });
      expect([
        0b11101010,
        0b10101111
      ]);
    });

    it('should encode number using multiple bits', function() {
      for (var i = 0; i < 8; i++)
        encoder.encodeBits(0b1011010, 7);
      expect([
        0b10110101,
        0b01101010,
        0b11010101,
        0b10101011,
        0b01010110,
        0b10101101,
        0b01011010
      ]);
    });
  });

  describe('integer', function() {
    it('should encode 10 in prefix-5 (C.1.1)', function() {
      encoder.skipBits(3);
      encoder.encodeInt(10);
      expect([ 0x0a ]);
    });

    it('should decode 1337 in prefix-5 (C.1.2)', function() {
      encoder.skipBits(3);
      encoder.encodeInt(1337);
      expect([
        0b00011111,
        0b10011010,
        0b00001010
      ]);
    });

    it('should decode 42 at octect boundary (C.1.3)', function() {
      encoder.encodeInt(42);
      expect([ 0b00101010 ]);
    });

    it('should regression 6-bit test', function() {
      encoder.skipBits(2);
      encoder.encodeInt(63);
      expect([ 0b00111111, 0b00000000 ]);
    });
  });

  describe('string', function() {
    it('should encode literal from (C.2.1)', function() {
      encoder.encodeStr(new Buffer('custom-key'), false);
      expect([ [ 0x0a ], 'custom-key' ]);
    });

    it('should encode literal from (C.4.1)', function() {
      encoder.encodeStr(new Buffer('www.example.com'), true);
      expect('8c' +
                 'f1e3 c2e5 f23a 6ba0 ab90 f4ff'.replace(/ /g, ''),
             'hex');
    });

    it('should decode literal from (C.4.2)', function() {
      encoder.encodeStr(new Buffer('no-cache'), true);
      expect(
        '86' +
            'a8eb 1064 9cbf'.replace(/ /g, ''),
        'hex');
    });

    it('should encode literal from (C.4.3) #1', function() {
      encoder.encodeStr(new Buffer('custom-key'), true);
      expect(
        '88' +
            '25a8 49e9 5ba9 7d7f'.replace(/ /g, ''),
        'hex');
    });

    it('should encode literal from (C.4.3) #2', function() {
      encoder.encodeStr(new Buffer('custom-value'), true);
      expect(
        '89' +
            '25a8 49e9 5bb8 e8b4 bf'.replace(/ /g, ''),
        'hex');
    });

    it('should encode literal from (C.6.1) #1', function() {
      encoder.encodeStr(new Buffer('Mon, 21 Oct 2013 20:13:21 GMT'), true);
      expect(
        ('96' +
            'd07a be94 1054 d444 a820 0595 040b 8166' +
            'e082 a62d 1bff').replace(/ /g, ''),
        'hex');
    });

    it('should encode literal from (C.6.1) #2', function() {
      encoder.encodeStr(new Buffer('https://www.example.com'), true);
      expect(
        ('91' +
            '9d29 ad17 1863 c78f 0b97 c8e9 ae82 ae43' +
            'd3').replace(/ /g, ''),
        'hex');
    });

    it('should encode many 5 bit chars', function() {
      encoder.encodeStr(new Buffer('eeeeeeee'), true);
      // e = 00101, 0x294A5294A5 = 00101 x 8
      expect('85294A5294A5', 'hex');
    });

    it('should encode many 5 bit chars with 3-bit EOS', function() {
      // e = 00101, EOS=111,
      // 0x294A5294A52F = 00101 x 9 + 111
      encoder.encodeStr(new Buffer('eeeeeeeee'), true);
      expect('86294A5294A52F', 'hex');
    });

    it('should decode many 5 bit chars with 2-bit EOS', function() {
      // e = 00101, EOS=11,
      // 0x294A5297 = 00101 x 6 + 11
      encoder.encodeStr(new Buffer('eeeeee'), true);
      expect('84294A5297', 'hex');
    });

    it('should decode many multi-octet chars', function() {
      encoder.encodeStr([ 1, 1, 1, 1, 1, 1, 1, 1 ], true);
      expect('97ffffb1ffff63fffec7fffd8ffffb1ffff63fffec7fffd8',
             'hex');
    });
  });
});


code:
exports.specExamples = [
  {
    id: 'C.3.1',
    huffman: false,
    input: '8286 8441 0f77 7777 2e65 7861 6d70 6c65' +
           '2e63 6f6d',
    output: [
      [ ':method', 'GET' ],
      [ ':scheme', 'http' ],
      [ ':path', '/' ],
      [ ':authority', 'www.example.com' ]
    ],
    table: [
      [ ':authority', 'www.example.com', 57 ]
    ]
  },
  {
    id: 'C.3.2',
    continuation: true,
    huffman: false,
    input: '8286 84be 5808 6e6f 2d63 6163 6865',
    output: [
      [ ':method', 'GET' ],
      [ ':scheme', 'http' ],
      [ ':path', '/' ],
      [ ':authority', 'www.example.com' ],
      [ 'cache-control', 'no-cache' ]
    ],
    table: [
      [ 'cache-control', 'no-cache', 53 ],
      [ ':authority', 'www.example.com', 57 ]
    ]
  },
  {
    id: 'C.3.3',
    continuation: true,
    huffman: false,
    input: '8287 85bf 400a 6375 7374 6f6d 2d6b 6579' +
           '0c63 7573 746f 6d2d 7661 6c75 65',
    output: [
      [ ':method', 'GET' ],
      [ ':scheme', 'https' ],
      [ ':path', '/index.html' ],
      [ ':authority', 'www.example.com' ],
      [ 'custom-key', 'custom-value' ]
    ],
    table: [
      [ 'custom-key', 'custom-value', 54 ],
      [ 'cache-control', 'no-cache', 53 ],
      [ ':authority', 'www.example.com', 57 ]
    ]
  },

  {
    id: 'C.4.1',
    input: '8286 8441 8cf1 e3c2 e5f2 3a6b a0ab 90f4' +
           'ff',
    output: [
      [ ':method', 'GET' ],
      [ ':scheme', 'http' ],
      [ ':path', '/' ],
      [ ':authority', 'www.example.com' ]
    ],
    table: [
      [ ':authority', 'www.example.com', 57 ]
    ]
  },
  {
    id: 'C.4.2',
    continuation: true,
    input: '8286 84be 5886 a8eb 1064 9cbf',
    output: [
      [ ':method', 'GET' ],
      [ ':scheme', 'http' ],
      [ ':path', '/' ],
      [ ':authority', 'www.example.com' ],
      [ 'cache-control', 'no-cache' ]
    ],
    table: [
      [ 'cache-control', 'no-cache', 53 ],
      [ ':authority', 'www.example.com', 57 ]
    ]
  },
  {
    id: 'C.4.3',
    continuation: true,
    input: '8287 85bf 4088 25a8 49e9 5ba9 7d7f 8925' +
           'a849 e95b b8e8 b4bf',
    output: [
      [ ':method', 'GET' ],
      [ ':scheme', 'https' ],
      [ ':path', '/index.html' ],
      [ ':authority', 'www.example.com' ],
      [ 'custom-key', 'custom-value' ]
    ],
    table: [
      [ 'custom-key', 'custom-value', 54 ],
      [ 'cache-control', 'no-cache', 53 ],
      [ ':authority', 'www.example.com', 57 ]
    ]
  },

  {
    id: 'C.5.1',
    huffman: false,
    input: '4803 3330 3258 0770 7269 7661 7465 611d' +
           '4d6f 6e2c 2032 3120 4f63 7420 3230 3133' +
           '2032 303a 3133 3a32 3120 474d 546e 1768' +
           '7474 7073 3a2f 2f77 7777 2e65 7861 6d70' +
           '6c65 2e63 6f6d',
    output: [
      [ ':status', '302' ],
      [ 'cache-control', 'private' ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT' ],
      [ 'location', 'https://www.example.com' ]
    ],
    table: [
      [ 'location', 'https://www.example.com', 63 ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT', 65 ],
      [ 'cache-control', 'private', 52 ],
      [ ':status', '302', 42 ]
    ]
  },
  {
    id: 'C.5.2',
    huffman: false,
    continuation: true,
    input: '4803 3330 37c1 c0bf',
    output: [
      [ ':status', '307' ],
      [ 'cache-control', 'private' ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT' ],
      [ 'location', 'https://www.example.com' ]
    ],
    table: [
      [ ':status', '307', 42 ],
      [ 'location', 'https://www.example.com', 63 ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT', 65 ],
      [ 'cache-control', 'private', 52 ]
    ]
  },
  {
    id: 'C.5.3',
    huffman: false,
    continuation: true,
    input: '88c1 611d 4d6f 6e2c 2032 3120 4f63 7420' +
           '3230 3133 2032 303a 3133 3a32 3220 474d' +
           '54c0 5a04 677a 6970 7738 666f 6f3d 4153' +
           '444a 4b48 514b 425a 584f 5157 454f 5049' +
           '5541 5851 5745 4f49 553b 206d 6178 2d61' +
           '6765 3d33 3630 303b 2076 6572 7369 6f6e' +
           '3d31',
    output: [
      [ ':status', '200' ],
      [ 'cache-control', 'private' ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:22 GMT' ],
      [ 'location', 'https://www.example.com' ],
      [ 'content-encoding', 'gzip' ],
      [ 'set-cookie',
        'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1' ]
    ],
    table: [
      [ 'set-cookie',
        'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1',
        98 ],
      [ 'content-encoding', 'gzip', 52 ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:22 GMT', 65 ]
    ]
  },

  {
    id: 'C.6.1',
    input: '4882 6402 5885 aec3 771a 4b61 96d0 7abe' +
           '9410 54d4 44a8 2005 9504 0b81 66e0 82a6' +
           '2d1b ff6e 919d 29ad 1718 63c7 8f0b 97c8' +
           'e9ae 82ae 43d3',
    output: [
      [ ':status', '302' ],
      [ 'cache-control', 'private' ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT' ],
      [ 'location', 'https://www.example.com' ]
    ],
    table: [
      [ 'location', 'https://www.example.com', 63 ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT', 65 ],
      [ 'cache-control', 'private', 52 ],
      [ ':status', '302', 42 ]
    ]
  },
  {
    id: 'C.6.2',
    continuation: true,
    input: '4883 640e ffc1 c0bf',
    output: [
      [ ':status', '307' ],
      [ 'cache-control', 'private' ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT' ],
      [ 'location', 'https://www.example.com' ]
    ],
    table: [
      [ ':status', '307', 42 ],
      [ 'location', 'https://www.example.com', 63 ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:21 GMT', 65 ],
      [ 'cache-control', 'private', 52 ]
    ]
  },
  {
    id: 'C.6.3',
    continuation: true,
    input: '88c1 6196 d07a be94 1054 d444 a820 0595' +
           '040b 8166 e084 a62d 1bff c05a 839b d9ab' +
           '77ad 94e7 821d d7f2 e6c7 b335 dfdf cd5b' +
           '3960 d5af 2708 7f36 72c1 ab27 0fb5 291f' +
           '9587 3160 65c0 03ed 4ee5 b106 3d50 07',
    output: [
      [ ':status', '200' ],
      [ 'cache-control', 'private' ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:22 GMT' ],
      [ 'location', 'https://www.example.com' ],
      [ 'content-encoding', 'gzip' ],
      [ 'set-cookie',
        'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1' ]
    ],
    table: [
      [ 'set-cookie',
        'foo=ASDJKHQKBZXOQWEOPIUAXQWEOIU; max-age=3600; version=1',
        98 ],
      [ 'content-encoding', 'gzip', 52 ],
      [ 'date', 'Mon, 21 Oct 2013 20:13:22 GMT', 65 ]
    ]
  }
];


code:
var utils = require('./utils');

var table = `
1	:authority
2	:method	GET
3	:method	POST
4	:path	/
5	:path	/index.html
6	:scheme	http
7	:scheme	https
8	:status	200
9	:status	204
10	:status	206
11	:status	304
12	:status	400
13	:status	404
14	:status	500
15	accept-charset
16	accept-encoding	gzip, deflate
17	accept-language
18	accept-ranges
19	accept
20	access-control-allow-origin
21	age
22	allow
23	authorization
24	cache-control
25	content-disposition
26	content-encoding
27	content-language
28	content-length
29	content-location
30	content-range
31	content-type
32	cookie
33	date
34	etag
35	expect
36	expires
37	from
38	host
39	if-match
40	if-modified-since
41	if-none-match
42	if-range
43	if-unmodified-since
44	last-modified
45	link
46	location
47	max-forwards
48	proxy-authenticate
49	proxy-authorization
50	range
51	referer
52	refresh
53	retry-after
54	server
55	set-cookie
56	strict-transport-security
57	transfer-encoding
58	user-agent
59	vary
60	via
61	www-authenticate
`;

var out = [];
table.split('\n').filter(function(line) {
  return line;
}).forEach(function(line) {
  var columns = line.split(/\t/g);
  var name = columns[1];
  var value = columns[2];
  var nameSize = Buffer.byteLength(name);
  var valueSize = Buffer.byteLength(value);
  out.push({
    name: name,
    value: value,
    nameSize: nameSize,
    totalSize: nameSize + valueSize + 32
  });
});

console.log('exports.table = ' + JSON.stringify(out, false, 2) + ';');

var map = {};
table.split('\n').filter(function(line) {
  return line;
}).forEach(function(line) {
  var columns = line.split(/\t/g);
  var name = columns[1];
  var value = columns[2];

  var index = columns[0] | 0;
  if (!map[name]) {
    map[name] = {
      index: index,
      values: {}
    };
  }
  map[name].values[value] = index;
});
console.log('exports.map = ' + JSON.stringify(map, false, 2) + ';');


code:
// Wrap lines after 79 chars
exports.wrap = function wrap(str) {
  var out = [];
  var pad = '    ';
  var line = pad;

  var chunks = str.split(/,/g);
  chunks.forEach(function(chunk, i) {
    var append = chunk;
    if (i !== chunks.length - 1)
      append += ',';

    if (line.length + append.length > 79) {
      out.push(line);
      line = pad;
    }
    line += append;
  });
  if (line !== pad)
    out.push(line);

  return out.join('\n');
};


code:
html-entities
=============

Fastest HTML entities library.

Comes with both TypeScript and Flow types.

Installation
------------

```bash
$ npm install html-entities
```

Usage
-----

### encode(text, options)

Encodes text replacing HTML special characters (`<>&"'`) and/or other character ranges depending on `mode` option value.

```js
import {encode} from 'html-entities';

encode('< > " \' & © ∆');
// -> '< > " ' & © ∆'

encode('< ©', {mode: 'nonAsciiPrintable'});
// -> '< ©'

encode('< ©', {mode: 'nonAsciiPrintable', level: 'xml'});
// -> '< ©'

encode('< > " \' & ©', {mode: 'nonAsciiPrintableOnly', level: 'xml'});
// -> '< > " \' & ©'
```

Options:

#### level

 * `all` alias to `html5` (default).
 * `html5` uses `HTML5` named references.
 * `html4` uses `HTML4` named references.
 * `xml` uses `XML` named references.

#### mode

 * `specialChars` encodes only HTML special characters (default).
 * `nonAscii` encodes HTML special characters and everything outside the [ASCII character range](https://en.wikipedia.org/wiki/ASCII).
 * `nonAsciiPrintable` encodes HTML special characters and everything outiside of the [ASCII printable characters](https://en.wikipedia.org/wiki/ASCII#Printable_characters).
 * `nonAsciiPrintableOnly` everything outiside of the [ASCII printable characters](https://en.wikipedia.org/wiki/ASCII#Printable_characters) keeping HTML special characters intact.
 * `extensive` encodes all non-printable characters, non-ASCII characters and all characters with named references.

#### numeric

 * `decimal` uses decimal numbers when encoding html entities. i.e. `©` (default).
 * `hexadecimal` uses hexadecimal numbers when encoding html entities. i.e. `©`.

### decode(text, options)

Decodes text replacing entities to characters. Unknown entities are left as is.

```js
import {decode} from 'html-entities';

decode('< > " ' & © ∆');
// -> '< > " \' & © ∆'

decode('©', {level: 'html5'});
// -> '©'

decode('©', {level: 'xml'});
// -> '©'
```

Options:

#### level

 * `all` alias to `html5` (default).
 * `html5` uses `HTML5` named references.
 * `html4` uses `HTML4` named references.
 * `xml` uses `XML` named references.

#### scope

 * `body` emulates behavior of browser when parsing tag bodies: entities without semicolon are also replaced (default).
 * `attribute` emulates behavior of browser when parsing tag attributes: entities without semicolon are replaced when not followed by equality sign `=`.
 * `strict` ignores entities without semicolon.

### decodeEntity(text, options)

Decodes a single HTML entity. Unknown entitiy is left as is.

```js
import {decodeEntity} from 'html-entities';

decodeEntity('<');
// -> '<'

decodeEntity('©', {level: 'html5'});
// -> '©'

decodeEntity('©', {level: 'xml'});
// -> '©'
```

Options:

#### level

 * `all` alias to `html5` (default).
 * `html5` uses `HTML5` named references.
 * `html4` uses `HTML4` named references.
 * `xml` uses `XML` named references.

Performance
-----------

Statistically significant comparison with other libraries using `benchmark.js`.
Results by this library are marked with `*`.
The source code of the benchmark is available at `benchmark/benchmark.ts`.

```
Common

    Initialization / Load speed

      * #1: html-entities x 2,632,942 ops/sec ±3.71% (72 runs sampled)
        #2: entities x 1,379,154 ops/sec ±5.87% (75 runs sampled)
        #3: he x 1,334,035 ops/sec ±3.14% (83 runs sampled)

HTML5

    Encode test

      * #1: html-entities.encode - html5, nonAscii x 415,806 ops/sec ±0.73% (85 runs sampled)
      * #2: html-entities.encode - html5, nonAsciiPrintable x 401,420 ops/sec ±0.35% (93 runs sampled)
        #3: entities.encodeNonAsciiHTML x 401,235 ops/sec ±0.41% (88 runs sampled)
        #4: entities.encodeHTML x 284,868 ops/sec ±0.45% (93 runs sampled)
      * #5: html-entities.encode - html5, extensive x 237,613 ops/sec ±0.42% (93 runs sampled)
        #6: he.encode x 91,459 ops/sec ±0.50% (84 runs sampled)

    Decode test

        #1: entities.decodeHTMLStrict x 614,920 ops/sec ±0.41% (89 runs sampled)
        #2: entities.decodeHTML x 577,698 ops/sec ±0.44% (90 runs sampled)
      * #3: html-entities.decode - html5, strict x 323,680 ops/sec ±0.39% (92 runs sampled)
      * #4: html-entities.decode - html5, body x 297,548 ops/sec ±0.45% (91 runs sampled)
      * #5: html-entities.decode - html5, attribute x 293,617 ops/sec ±0.37% (94 runs sampled)
        #6: he.decode x 145,383 ops/sec ±0.36% (94 runs sampled)

HTML4

    Encode test

      * #1: html-entities.encode - html4, nonAscii x 379,799 ops/sec ±0.29% (96 runs sampled)
      * #2: html-entities.encode - html4, nonAsciiPrintable x 350,003 ops/sec ±0.42% (92 runs sampled)
      * #3: html-entities.encode - html4, extensive x 169,759 ops/sec ±0.43% (90 runs sampled)

    Decode test

      * #1: html-entities.decode - html4, attribute x 291,048 ops/sec ±0.42% (92 runs sampled)
      * #2: html-entities.decode - html4, strict x 287,110 ops/sec ±0.56% (93 runs sampled)
      * #3: html-entities.decode - html4, body x 285,529 ops/sec ±0.57% (93 runs sampled)

XML

    Encode test

        #1: entities.encodeXML x 418,561 ops/sec ±0.80% (90 runs sampled)
      * #2: html-entities.encode - xml, nonAsciiPrintable x 402,868 ops/sec ±0.30% (89 runs sampled)
      * #3: html-entities.encode - xml, nonAscii x 403,669 ops/sec ±7.87% (83 runs sampled)
      * #4: html-entities.encode - xml, extensive x 237,766 ops/sec ±0.45% (93 runs sampled)

    Decode test

        #1: entities.decodeXML x 888,700 ops/sec ±0.48% (93 runs sampled)
      * #2: html-entities.decode - xml, strict x 353,127 ops/sec ±0.40% (92 runs sampled)
      * #3: html-entities.decode - xml, body x 355,796 ops/sec ±1.58% (86 runs sampled)
      * #4: html-entities.decode - xml, attribute x 369,454 ops/sec ±8.74% (84 runs sampled)

Escaping

    Escape test

        #1: entities.escapeUTF8 x 1,308,013 ops/sec ±0.37% (91 runs sampled)
      * #2: html-entities.encode - xml, specialChars x 1,258,760 ops/sec ±1.00% (93 runs sampled)
        #3: he.escape x 822,569 ops/sec ±0.24% (94 runs sampled)
        #4: entities.escape x 434,243 ops/sec ±0.34% (91 runs sampled)
```

License
-------

MIT

Security contact information
----------------------------

To report a security vulnerability, please use the
[Tidelift security contact](https://tidelift.com/security). Tidelift will
coordinate the fix and disclosure.

`html-entities` for enterprise
------------------------------

Available as part of the Tidelift Subscription

The maintainers of `html-entities` and thousands of other packages are working with
Tidelift to deliver commercial support and maintenance for the open source
dependencies you use to build your applications. Save time, reduce risk, and
improve code health, while paying the maintainers of the exact dependencies you
use.
[Learn more.](https://tidelift.com/subscription/pkg/npm-html-entities?utm_source=npm-html-entities&utm_medium=referral&utm_campaign=enterprise)


code:
export declare type Level = 'xml' | 'html4' | 'html5' | 'all';
interface CommonOptions {
    level?: Level;
}
export declare type EncodeMode = 'specialChars' | 'nonAscii' | 'nonAsciiPrintable' | 'nonAsciiPrintableOnly' | 'extensive';
export interface EncodeOptions extends CommonOptions {
    mode?: EncodeMode;
    numeric?: 'decimal' | 'hexadecimal';
}
export declare type DecodeScope = 'strict' | 'body' | 'attribute';
export interface DecodeOptions extends CommonOptions {
    scope?: DecodeScope;
}
/** Encodes all the necessary (specified by `level`) characters in the text */
export declare function encode(text: string | undefined | null, { mode, numeric, level }?: EncodeOptions): string;
/** Decodes a single entity */
export declare function decodeEntity(entity: string | undefined | null, { level }?: CommonOptions): string;
/** Decodes all entities in the text */
export declare function decode(text: string | undefined | null, { level, scope }?: DecodeOptions): string;
export {};


code:
/**
 * Flowtype definitions for index
 * Generated by Flowgen from a Typescript Definition
 * Flowgen v1.13.0
 * @flow
 */

export type Level = "xml" | "html4" | "html5" | "all";
declare interface CommonOptions {
  level?: Level;
}
export type EncodeMode =
  | "specialChars"
  | "nonAscii"
  | "nonAsciiPrintable"
  | "nonAsciiPrintableOnly"
  | "extensive";
export type EncodeOptions = {
  mode?: EncodeMode,
  numeric?: "decimal" | "hexadecimal",
  ...
} & CommonOptions;
export type DecodeScope = "strict" | "body" | "attribute";
export type DecodeOptions = {
  scope?: DecodeScope,
  ...
} & CommonOptions;

/**
 * Encodes all the necessary (specified by `level`) characters in the text
 */
declare export function encode(
  text: string | void | null,
  x?: EncodeOptions
): string;

/**
 * Decodes a single entity
 */
declare export function decodeEntity(
  entity: string | void | null,
  x?: CommonOptions
): string;

/**
 * Decodes all entities in the text
 */
declare export function decode(
  text: string | void | null,
  x?: DecodeOptions
): string;
declare export {};


names: exports fromCodePoint String astralCodePoint fromCharCode Math floor getCodePoint prototype codePointAt input position charCodeAt highSurrogateFrom highSurrogateTo


html-entities

description: Fastest HTML entities encode/decode library.

keywords:
html
html entities
html entities encode
html entities decode
entities
entities encode
entities decode

type: github

type: patreon

test: mocha --recursive -r ts-node/register test/**/*.ts

test:watch: mocha -w --recursive -r ts-node/register test/**/*.ts

test:lib: TEST_LIB=1 npm run test

benchmark: ts-node benchmark/benchmark

lint: eslint src/**.ts

flow-type-gen: flowgen --add-flow-header lib/index.d.ts -o lib/index.js.flow

remove-unused-declarations: find lib -type f \( -name '*.d.ts' ! -name index.d.ts \) | xargs rm

minimize-lib-files: cd lib && find . -type f \( -name '*.js' \) | while read fn; do terser --source-map "content='$fn.map',filename='$fn.map',url='$fn.map'" $fn -o $fn; done

build: rm -Rf lib/* && tsc && npm run remove-unused-declarations && npm run flow-type-gen && npm run minimize-lib-files && npm run test:lib

prepublishOnly: npm run build

files: lib src LICENSE

pre-commit: npm run lint && npm run test


code:
import {bodyRegExps, namedReferences} from './named-references';
import {numericUnicodeMap} from './numeric-unicode-map';
import {fromCodePoint, getCodePoint} from './surrogate-pairs';

const allNamedReferences = {
    ...namedReferences,
    all: namedReferences.html5
};

function replaceUsingRegExp(macroText: string, macroRegExp: RegExp, macroReplacer: (input: string) => string): string {
    macroRegExp.lastIndex = 0;
    let replaceMatch = macroRegExp.exec(macroText);
    let replaceResult;
    if (replaceMatch) {
        replaceResult = '';
        let replaceLastIndex = 0;
        do {
            if (replaceLastIndex !== replaceMatch.index) {
                replaceResult += macroText.substring(replaceLastIndex, replaceMatch.index);
            }
            const replaceInput = replaceMatch[0];
            replaceResult += macroReplacer(replaceInput);
            replaceLastIndex = replaceMatch.index + replaceInput.length;
        } while ((replaceMatch = macroRegExp.exec(macroText)));

        if (replaceLastIndex !== macroText.length) {
            replaceResult += macroText.substring(replaceLastIndex);
        }
    } else {
        replaceResult = macroText;
    }
    return replaceResult;
}

export type Level = 'xml' | 'html4' | 'html5' | 'all';

interface CommonOptions {
    level?: Level;
}

export type EncodeMode = 'specialChars' | 'nonAscii' | 'nonAsciiPrintable' | 'nonAsciiPrintableOnly' | 'extensive';

export interface EncodeOptions extends CommonOptions {
    mode?: EncodeMode;
    numeric?: 'decimal' | 'hexadecimal';
}

export type DecodeScope = 'strict' | 'body' | 'attribute';

export interface DecodeOptions extends CommonOptions {
    scope?: DecodeScope;
}

const encodeRegExps: Record<EncodeMode, RegExp> = {
    specialChars: /[<>'"&]/g,
    nonAscii: /[<>'"&\u0080-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g,
    nonAsciiPrintable: /[<>'"&\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g,
    nonAsciiPrintableOnly: /[\x01-\x08\x11-\x15\x17-\x1F\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g,
    extensive: /[\x01-\x0c\x0e-\x1f\x21-\x2c\x2e-\x2f\x3a-\x40\x5b-\x60\x7b-\x7d\x7f-\uD7FF\uE000-\uFFFF]|[\uD800-\uDBFF][\uDC00-\uDFFF]|[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?:[^\uD800-\uDBFF]|^)[\uDC00-\uDFFF]/g
};

const defaultEncodeOptions: EncodeOptions = {
    mode: 'specialChars',
    level: 'all',
    numeric: 'decimal'
};

/** Encodes all the necessary (specified by `level`) characters in the text */
export function encode(
    text: string | undefined | null,
    {mode = 'specialChars', numeric = 'decimal', level = 'all'}: EncodeOptions = defaultEncodeOptions
) {
    if (!text) {
        return '';
    }

    const encodeRegExp = encodeRegExps[mode];
    const references = allNamedReferences[level].characters;
    const isHex = numeric === 'hexadecimal';

    return replaceUsingRegExp(text, encodeRegExp, (input) => {
        let result = references[input];
        if (!result) {
            const code = input.length > 1 ? getCodePoint(input, 0)! : input.charCodeAt(0);
            result = (isHex ? '&#x' + code.toString(16) : '&#' + code) + ';';
        }
        return result;
    });
}

const defaultDecodeOptions: DecodeOptions = {
    scope: 'body',
    level: 'all'
};

const strict = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+);/g;
const attribute = /&(?:#\d+|#[xX][\da-fA-F]+|[0-9a-zA-Z]+)[;=]?/g;

const baseDecodeRegExps: Record<Exclude<Level, 'all'>, Record<DecodeScope, RegExp>> = {
    xml: {
        strict,
        attribute,
        body: bodyRegExps.xml
    },
    html4: {
        strict,
        attribute,
        body: bodyRegExps.html4
    },
    html5: {
        strict,
        attribute,
        body: bodyRegExps.html5
    }
};

const decodeRegExps: Record<Level, Record<DecodeScope, RegExp>> = {
    ...baseDecodeRegExps,
    all: baseDecodeRegExps.html5
};

const fromCharCode = String.fromCharCode;
const outOfBoundsChar = fromCharCode(65533);

const defaultDecodeEntityOptions: CommonOptions = {
    level: 'all'
};

function getDecodedEntity(
    entity: string,
    references: Record<string, string>,
    isAttribute: boolean,
    isStrict: boolean
): string {
    let decodeResult = entity;
    const decodeEntityLastChar = entity[entity.length - 1];
    if (isAttribute && decodeEntityLastChar === '=') {
        decodeResult = entity;
    } else if (isStrict && decodeEntityLastChar !== ';') {
        decodeResult = entity;
    } else {
        const decodeResultByReference = references[entity];
        if (decodeResultByReference) {
            decodeResult = decodeResultByReference;
        } else if (entity[0] === '&' && entity[1] === '#') {
            const decodeSecondChar = entity[2];
            const decodeCode =
                decodeSecondChar == 'x' || decodeSecondChar == 'X'
                    ? parseInt(entity.substr(3), 16)
                    : parseInt(entity.substr(2));

            decodeResult =
                decodeCode >= 0x10ffff
                    ? outOfBoundsChar
                    : decodeCode > 65535
                    ? fromCodePoint(decodeCode)
                    : fromCharCode(numericUnicodeMap[decodeCode] || decodeCode);
        }
    }
    return decodeResult;
}

/** Decodes a single entity */
export function decodeEntity(
    entity: string | undefined | null,
    {level = 'all'}: CommonOptions = defaultDecodeEntityOptions
): string {
    if (!entity) {
        return '';
    }
    return getDecodedEntity(entity, allNamedReferences[level].entities, false, false);
}

/** Decodes all entities in the text */
export function decode(
    text: string | undefined | null,
    {level = 'all', scope = level === 'xml' ? 'strict' : 'body'}: DecodeOptions = defaultDecodeOptions
) {
    if (!text) {
        return '';
    }

    const decodeRegExp = decodeRegExps[level][scope];
    const references = allNamedReferences[level].entities;
    const isAttribute = scope === 'attribute';
    const isStrict = scope === 'strict';

    return replaceUsingRegExp(text, decodeRegExp, (entity) =>
        getDecodedEntity(entity, references, isAttribute, isStrict)
    );
}


code:
export const fromCodePoint =
    String.fromCodePoint ||
    function (astralCodePoint: number) {
        return String.fromCharCode(
            Math.floor((astralCodePoint - 0x10000) / 0x400) + 0xd800,
            ((astralCodePoint - 0x10000) % 0x400) + 0xdc00
        );
    };

export const getCodePoint = String.prototype.codePointAt
    ? function (input: string, position: number) {
          return input.codePointAt(position);
      }
    : function (input: string, position: number) {
          return (input.charCodeAt(position) - 0xd800) * 0x400 + input.charCodeAt(position + 1) - 0xdc00 + 0x10000;
      };

export const highSurrogateFrom = 0xd800;
export const highSurrogateTo = 0xdbff;


code:
# html-escaper [![Build Status](https://travis-ci.org/WebReflection/html-escaper.svg?branch=master)](https://travis-ci.org/WebReflection/html-escaper) [![Coverage Status](https://coveralls.io/repos/github/WebReflection/html-escaper/badge.svg?branch=master)](https://coveralls.io/github/WebReflection/html-escaper?branch=master)
A simple module to escape/unescape common problematic entities.

### How
This package is available in npm so `npm install html-escaper` is all you need to do, using eventually the global flag too.

Once the module is present
```js
var html = require('html-escaper');

// two basic methods
html.escape('string');
html.unescape('escaped string');
```

### Why
there is basically one rule only: do not **ever** replace one char after another if you are transforming a string into another.

```js
// WARNING: THIS IS WRONG
// if you are that kind of dev that does this
function escape(s) {
  return s.replace(/&/g, "&")
          .replace(/</g, "<")
          .replace(/>/g, ">")
          .replace(/'/g, "'")
          .replace(/"/g, """);
}

// you might be the same dev that does this too
function unescape(s) {
  return s.replace(/&/g, "&")
          .replace(/</g, "<")
          .replace(/>/g, ">")
          .replace(/'/g, "'")
          .replace(/"/g, '"');
}

// guess what we have here ?
unescape('&lt;');

// now guess this XSS too ...
unescape('&lt;script&gt;alert("yo")&lt;/script&gt;');

```

The last example will produce `<script>alert("yo")</script>` instead of the expected `<script>alert("yo")</script>`.

Nothing like this could possibly happen if we grab all chars at once and either ways.
It's just a fortunate case that after swapping `&` with `&` no other replace will be affected, but it's not portable and universally a bad practice.

Grab all chars at once, no excuses!

**more details**
As somebody might think it's an `unescape` issue only, it's not. Being an anti-pattern with side effects works both ways.

As example, changing the order of the replacement in escaping would produce the unexpected:
```js
function escape(s) {
  return s.replace(/</g, "<")
          .replace(/>/g, ">")
          .replace(/'/g, "'")
          .replace(/"/g, """)
          .replace(/&/g, "&");
}

escape('<'); // &lt; instead of <
```
If we do not want to code with the fear that the order wasn't perfect or that our order in either escaping or unescaping is different from the order another method or function used, if we understand the issue and we agree it's potentially a disaster prone approach, if we add the fact in this case creating 4 RegExp objects each time and invoking 4 times `.replace` trough the `String.prototype` is also potentially slower than creating one function only holding one object, or holding the function too, we should agree there is not absolutely any valid reason to keep proposing a char-by-char implementation.

We have proofs this approach can fail already so ... why should we risk? Just avoid and grab all chars at once or simply use this tiny utility.

### Backtick
Internt explorer < 9 has [some backtick issue](https://html5sec.org/#102)

For compatibility sake with common server-side HTML entities encoders and decoders, and in order to have the most reliable I/O, this little utility will NOT fix this IE < 9 problem.

It is also important to note that if we create valid HTML and we set attributes at runtime through this utility, backticks in strings cannot possibly affect attribute behaviors.

```js
var img = new Image();
img.src = html.escape(
  'x` `<script>alert(1)</script>"` `'
);
// it won't cause problems even in IE < 9
```

**However**, if you use `innerHTML` and you target IE < 9 then [this **might** be a problem](https://github.com/nette/nette/issues/1496).

Accordingly, if you need more chars and/or backticks to be escaped and unescaped, feel free to use alternatives like [lodash](https://github.com/lodash/lodash) or [he](https://www.npmjs.com/package/he)

Here a bit more of [my POV](https://github.com/WebReflection/html-escaper/commit/52d554fc6e8583b6ffdd357967cf71962fc07cf6#commitcomment-10625122) and why I haven't implemented same thing alternatives did. Good news: those are alternatives ;-)


code:
'use strict';
/**
 * Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

var replace = ''.replace;

var ca = /[&<>'"]/g;
var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;

var esca = {
  '&': '&',
  '<': '<',
  '>': '>',
  "'": ''',
  '"': '"'
};
var unes = {
  '&': '&',
  '&': '&',
  '<': '<',
  '<': '<',
  '>': '>',
  '>': '>',
  ''': "'",
  ''': "'",
  '"': '"',
  '"': '"'
};

function escape(es) {
  return replace.call(es, ca, pe);
}
exports.escape = escape;

function unescape(un) {
  return replace.call(un, es, cape);
}
exports.unescape = unescape;

function pe(m) {
  return esca[m];
}

function cape(m) {
  return unes[m];
}


code:
/**
 * Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

var replace = ''.replace;

var ca = /[&<>'"]/g;
var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;

var esca = {
  '&': '&',
  '<': '<',
  '>': '>',
  "'": ''',
  '"': '"'
};
var unes = {
  '&': '&',
  '&': '&',
  '<': '<',
  '<': '<',
  '>': '>',
  '>': '>',
  ''': "'",
  ''': "'",
  '"': '"',
  '"': '"'
};

export function escape(es) {
  return replace.call(es, ca, pe);
};

export function unescape(un) {
  return replace.call(un, es, cape);
};

function pe(m) {
  return esca[m];
}

function cape(m) {
  return unes[m];
}


code:
var html = (function (exports) {
  'use strict';

  /**
   * Copyright (C) 2017-present by Andrea Giammarchi - @WebReflection
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to deal
   * in the Software without restriction, including without limitation the rights
   * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   * copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
   * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
   * THE SOFTWARE.
   */

  var replace = ''.replace;

  var ca = /[&<>'"]/g;
  var es = /&(?:amp|#38|lt|#60|gt|#62|apos|#39|quot|#34);/g;

  var esca = {
    '&': '&',
    '<': '<',
    '>': '>',
    "'": ''',
    '"': '"'
  };
  var unes = {
    '&': '&',
    '&': '&',
    '<': '<',
    '<': '<',
    '>': '>',
    '>': '>',
    ''': "'",
    ''': "'",
    '"': '"',
    '"': '"'
  };

  function escape(es) {
    return replace.call(es, ca, pe);
  }
  function unescape(un) {
    return replace.call(un, es, cape);
  }
  function pe(m) {
    return esca[m];
  }

  function cape(m) {
    return unes[m];
  }

  exports.escape = escape;
  exports.unescape = unescape;

  return exports;

}({}));


html-escaper

description: fast and safe way to escape and unescape &<>'" chars

build: npm run cjs && npm run rollup && npm run minify && npm test && npm run size

cjs: ascjs esm cjs

coveralls: cat ./coverage/lcov.info | coveralls

minify: uglifyjs index.js --comments=/^!/ --compress --mangle -o min.js

rollup: rollup --config rollup.config.js

size: cat index.js | wc -c;cat min.js | wc -c;gzip -c min.js | wc -c

test: istanbul cover ./test/index.js

keywords: html escape encode unescape decode entities

author: Andrea Giammarchi


code:
delete Object.freeze;

var html = require('../cjs');

console.assert(
  html.escape('&<>\'"') === '&<>'"',
  'correct escape'
);

console.assert(
  html.escape('<>\'"&') === '<>'"&',
  'correct inverted escape'
);

console.assert(
  '&<>\'"' === html.unescape('&<>'"'),
  'correct unescape'
);

console.assert(
  '<>\'"&' === html.unescape('<>'"&'),
  'correct inverted unescape'
);


code:
# HTMLMinifier

[![NPM version](https://img.shields.io/npm/v/html-minifier-terser.svg)](https://www.npmjs.com/package/html-minifier-terser)
[![Build Status](https://github.com/terser/html-minifier-terser/workflows/CI/badge.svg)](https://github.com/terser/html-minifier-terser/actions?workflow=CI)

[HTMLMinifier](https://terser.org/html-minifier-terser/) is a highly **configurable**, **well-tested**, JavaScript-based HTML minifier.

See [corresponding blog post](http://perfectionkills.com/experimenting-with-html-minifier/) for all the gory details of [how it works](http://perfectionkills.com/experimenting-with-html-minifier/#how_it_works), [description of each option](http://perfectionkills.com/experimenting-with-html-minifier/#options), [testing results](http://perfectionkills.com/experimenting-with-html-minifier/#field_testing) and [conclusions](http://perfectionkills.com/experimenting-with-html-minifier/#cost_and_benefits).

[Test suite is available online](https://terser.org/html-minifier-terser/tests/).

Also see corresponding [Ruby wrapper](https://github.com/stereobooster/html_minifier), and for Node.js, [Grunt plugin](https://github.com/gruntjs/grunt-contrib-htmlmin), [Gulp module](https://github.com/jonschlinkert/gulp-htmlmin), [Koa middleware wrapper](https://github.com/koajs/html-minifier) and [Express middleware wrapper](https://github.com/melonmanchan/express-minify-html).

For lint-like capabilities take a look at [HTMLLint](https://github.com/kangax/html-lint).

## Minification comparison

How does HTMLMinifier compare to other solutions — [HTML Minifier from Will Peavy](http://www.willpeavy.com/minifier/) (1st result in [Google search for "html minifier"](https://www.google.com/#q=html+minifier)) as well as [htmlcompressor.com](http://htmlcompressor.com) and [minimize](https://github.com/Swaagie/minimize)?

| Site                                                                         | Original size *(KB)* | HTMLMinifier | minimize | Will Peavy | htmlcompressor.com |
| ---------------------------------------------------------------------------- |:--------------------:| ------------:| --------:| ----------:| ------------------:|
| [Google](https://www.google.com/)                                            | 45                   | **41**       | 45       | 46         | 45                 |
| [Stack Overflow](https://stackoverflow.com/)                                 | 115                  | **82**       | 90       | 92         | 88                 |
| [HTMLMinifier](https://github.com/kangax/html-minifier)                      | 132                  | **105**      | 118      | 123        | 118                |
| [Bootstrap CSS](https://getbootstrap.com/docs/3.3/css/)                      | 271                  | **260**      | 269      | 229        | 269                |
| [Twitter](https://twitter.com/)                                              | 289                  | **238**      | 282      | 314        | 282                |
| [BBC](https://www.bbc.co.uk/)                                                | 298                  | **233**      | 289      | 291        | 269                |
| [Wikipedia](https://en.wikipedia.org/wiki/President_of_the_United_States)    | 574                  | **468**      | 557      | 578        | 557                |
| [Amazon](https://www.amazon.co.uk/)                                          | 702                  | **343**      | 692      | 704        | n/a                |
| [NBC](https://www.nbc.com/)                                                  | 844                  | **805**      | 843      | 844        | n/a                |
| [Eloquent Javascript](https://eloquentjavascript.net/1st_edition/print.html) | 870                  | **815**      | 840      | 864        | n/a                |
| [New York Times](https://www.nytimes.com/)                                   | 1304                 | **1183**     | 1301     | 1295       | n/a                |
| [ES draft](https://tc39.github.io/ecma262/)                                  | 6347                 | **5686**     | 5863     | n/a        | n/a                |

## Options Quick Reference

Most of the options are disabled by default.

| Option                         | Description     | Default |
|--------------------------------|-----------------|---------|
| `caseSensitive`                | Treat attributes in case sensitive manner (useful for custom HTML tags) | `false` |
| `collapseBooleanAttributes`    | [Omit attribute values from boolean attributes](http://perfectionkills.com/experimenting-with-html-minifier/#collapse_boolean_attributes) | `false` |
| `collapseInlineTagWhitespace`  | Don't leave any spaces between `display:inline;` elements when collapsing. Must be used in conjunction with `collapseWhitespace=true` | `false` |
| `collapseWhitespace`           | [Collapse white space that contributes to text nodes in a document tree](http://perfectionkills.com/experimenting-with-html-minifier/#collapse_whitespace) | `false` |
| `conservativeCollapse`         | Always collapse to 1 space (never remove it entirely). Must be used in conjunction with `collapseWhitespace=true` | `false` |
| `continueOnParseError`         | [Handle parse errors](https://html.spec.whatwg.org/multipage/parsing.html#parse-errors) instead of aborting. | `false` |
| `customAttrAssign`             | Arrays of regex'es that allow to support custom attribute assign expressions (e.g. `'<div flex?="{{mode != cover}}"></div>'`) | `[ ]` |
| `customAttrCollapse`           | Regex that specifies custom attribute to strip newlines from (e.g. `/ng-class/`) | |
| `customAttrSurround`           | Arrays of regex'es that allow to support custom attribute surround expressions (e.g. `<input {{#if value}}checked="checked"{{/if}}>`) | `[ ]` |
| `customEventAttributes`        | Arrays of regex'es that allow to support custom event attributes for `minifyJS` (e.g. `ng-click`) | `[ /^on[a-z]{3,}$/ ]` |
| `decodeEntities`               | Use direct Unicode characters whenever possible | `false` |
| `html5`                        | Parse input according to HTML5 specifications | `true` |
| `ignoreCustomComments`         | Array of regex'es that allow to ignore certain comments, when matched | `[ /^!/, /^\s*#/ ]` |
| `ignoreCustomFragments`        | Array of regex'es that allow to ignore certain fragments, when matched (e.g. `<?php ... ?>`, `{{ ... }}`, etc.)  | `[ /<%[\s\S]*?%>/, /<\?[\s\S]*?\?>/ ]` |
| `includeAutoGeneratedTags`     | Insert tags generated by HTML parser | `true` |
| `keepClosingSlash`             | Keep the trailing slash on singleton elements | `false` |
| `maxLineLength`                | Specify a maximum line length. Compressed output will be split by newlines at valid HTML split-points |
| `minifyCSS`                    | Minify CSS in style elements and style attributes (uses [clean-css](https://github.com/jakubpawlowicz/clean-css)) | `false` (could be `true`, `Object`, `Function(text, type)`) |
| `minifyJS`                     | Minify JavaScript in script elements and event attributes (uses [Terser](https://github.com/terser/terser)) | `false` (could be `true`, `Object`, `Function(text, inline)`) |
| `minifyURLs`                   | Minify URLs in various attributes (uses [relateurl](https://github.com/stevenvachon/relateurl)) | `false` (could be `String`, `Object`, `Function(text)`) |
| `noNewlinesBeforeTagClose`     | Never add a newline before a tag that closes an element | `false` |
| `preserveLineBreaks`           | Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break. Must be used in conjunction with `collapseWhitespace=true` | `false` |
| `preventAttributesEscaping`    | Prevents the escaping of the values of attributes | `false` |
| `processConditionalComments`   | Process contents of conditional comments through minifier | `false` |
| `processScripts`               | Array of strings corresponding to types of script elements to process through minifier (e.g. `text/ng-template`, `text/x-handlebars-template`, etc.) | `[ ]` |
| `quoteCharacter`               | Type of quote to use for attribute values (' or ") | |
| `removeAttributeQuotes`        | [Remove quotes around attributes when possible](http://perfectionkills.com/experimenting-with-html-minifier/#remove_attribute_quotes) | `false` |
| `removeComments`               | [Strip HTML comments](http://perfectionkills.com/experimenting-with-html-minifier/#remove_comments) | `false` |
| `removeEmptyAttributes`        | [Remove all attributes with whitespace-only values](http://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_or_blank_attributes) | `false` (could be `true`, `Function(attrName, tag)`) |
| `removeEmptyElements`          | [Remove all elements with empty contents](http://perfectionkills.com/experimenting-with-html-minifier/#remove_empty_elements) | `false` |
| `removeOptionalTags`           | [Remove optional tags](http://perfectionkills.com/experimenting-with-html-minifier/#remove_optional_tags) | `false` |
| `removeRedundantAttributes`    | [Remove attributes when value matches default.](http://perfectionkills.com/experimenting-with-html-minifier/#remove_redundant_attributes) | `false` |
| `removeScriptTypeAttributes`   | Remove `type="text/javascript"` from `script` tags. Other `type` attribute values are left intact | `false` |
| `removeStyleLinkTypeAttributes`| Remove `type="text/css"` from `style` and `link` tags. Other `type` attribute values are left intact | `false` |
| `removeTagWhitespace`          | Remove space between attributes whenever possible. **Note that this will result in invalid HTML!** | `false` |
| `sortAttributes`               | [Sort attributes by frequency](#sorting-attributes--style-classes) | `false` |
| `sortClassName`                | [Sort style classes by frequency](#sorting-attributes--style-classes) | `false` |
| `trimCustomFragments`          | Trim white space around `ignoreCustomFragments`. | `false` |
| `useShortDoctype`              | [Replaces the `doctype` with the short (HTML5) doctype](http://perfectionkills.com/experimenting-with-html-minifier/#use_short_doctype) | `false` |

### Sorting attributes / style classes

Minifier options like `sortAttributes` and `sortClassName` won't impact the plain-text size of the output. However, they form long repetitive chains of characters that should improve compression ratio of gzip used in HTTP compression.

## Special cases

### Ignoring chunks of markup

If you have chunks of markup you would like preserved, you can wrap them `<!-- htmlmin:ignore -->`.

### Preserving SVG tags

SVG tags are automatically recognized, and when they are minified, both case-sensitivity and closing-slashes are preserved, regardless of the minification settings used for the rest of the file.

### Working with invalid markup

HTMLMinifier **can't work with invalid or partial chunks of markup**. This is because it parses markup into a tree structure, then modifies it (removing anything that was specified for removal, ignoring anything that was specified to be ignored, etc.), then it creates a markup out of that tree and returns it.

Input markup (e.g. `<p id="">foo`)

↓

Internal representation of markup in a form of tree (e.g. `{ tag: "p", attr: "id", children: ["foo"] }`)

↓

Transformation of internal representation (e.g. removal of `id` attribute)

↓

Output of resulting markup (e.g. `<p>foo</p>`)

HTMLMinifier can't know that original markup was only half of the tree; it does its best to try to parse it as a full tree and it loses information about tree being malformed or partial in the beginning. As a result, it can't create a partial/malformed tree at the time of the output.

## Installation Instructions

From NPM for use as a command line app:

```shell
npm install html-minifier-terser -g
```

From NPM for programmatic use:

```shell
npm install html-minifier-terser
```

From Git:

```shell
git clone git://github.com/terser/html-minifier-terser.git
cd html-minifier-terser
npm link .
```

## Usage

Note that almost all options are disabled by default. For command line usage please see `html-minifier-terser --help` for a list of available options. Experiment and find what works best for you and your project.

- **Sample command line:** `html-minifier-terser --collapse-whitespace --remove-comments --remove-optional-tags --remove-redundant-attributes --remove-script-type-attributes --remove-tag-whitespace --use-short-doctype --minify-css true --minify-js true`

### Node.js

```js
const { minify } = require('html-minifier-terser');

const result = await minify('<p title="blah" id="moo">foo</p>', {
  removeAttributeQuotes: true
});
result; // '<p title=blah id=moo>foo</p>'
```

## Running benchmarks

Benchmarks for minified HTML:

```shell
node benchmark.js
```

## Running local server

```shell
npm run serve
```


code:
#!/usr/bin/env node
/**
 * html-minifier-terser CLI tool
 *
 * The MIT License (MIT)
 *
 *  Copyright (c) 2014-2016 Zoltan Frombach
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
 *  this software and associated documentation files (the "Software"), to deal in
 *  the Software without restriction, including without limitation the rights to
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 *  the Software, and to permit persons to whom the Software is furnished to do so,
 *  subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in all
 *  copies or substantial portions of the Software.
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 *
 */

'use strict';

var camelCase = require('camel-case').camelCase;
var fs = require('fs');
var info = require('./package.json');
var minify = require('./' + info.main).minify;
var paramCase = require('param-case').paramCase;
var path = require('path');
var { Command } = require('commander');

const program = new Command();
program.name(info.name);
program.version(info.version);

function fatal(message) {
  console.error(message);
  process.exit(1);
}

/**
 * JSON does not support regexes, so, e.g., JSON.parse() will not create
 * a RegExp from the JSON value `[ "/matchString/" ]`, which is
 * technically just an array containing a string that begins and end with
 * a forward slash. To get a RegExp from a JSON string, it must be
 * constructed explicitly in JavaScript.
 *
 * The likelihood of actually wanting to match text that is enclosed in
 * forward slashes is probably quite rare, so if forward slashes were
 * included in an argument that requires a regex, the user most likely
 * thought they were part of the syntax for specifying a regex.
 *
 * In the unlikely case that forward slashes are indeed desired in the
 * search string, the user would need to enclose the expression in a
 * second set of slashes:
 *
 *    --customAttrSrround "[\"//matchString//\"]"
 */
function parseRegExp(value) {
  if (value) {
    return new RegExp(value.replace(/^\/(.*)\/$/, '$1'));
  }
}

function parseJSON(value) {
  if (value) {
    try {
      return JSON.parse(value);
    }
    catch (e) {
      if (/^{/.test(value)) {
        fatal('Could not parse JSON value \'' + value + '\'');
      }
      return value;
    }
  }
}

function parseJSONArray(value) {
  if (value) {
    value = parseJSON(value);
    return Array.isArray(value) ? value : [value];
  }
}

function parseJSONRegExpArray(value) {
  value = parseJSONArray(value);
  return value && value.map(parseRegExp);
}

function parseString(value) {
  return value;
}

var mainOptions = {
  caseSensitive: 'Treat attributes in case sensitive manner (useful for SVG; e.g. viewBox)',
  collapseBooleanAttributes: 'Omit attribute values from boolean attributes',
  collapseInlineTagWhitespace: 'Collapse white space around inline tag',
  collapseWhitespace: 'Collapse white space that contributes to text nodes in a document tree.',
  conservativeCollapse: 'Always collapse to 1 space (never remove it entirely)',
  continueOnParseError: 'Handle parse errors instead of aborting',
  customAttrAssign: ['Arrays of regex\'es that allow to support custom attribute assign expressions (e.g. \'<div flex?="{{mode != cover}}"></div>\')', parseJSONRegExpArray],
  customAttrCollapse: ['Regex that specifies custom attribute to strip newlines from (e.g. /ng-class/)', parseRegExp],
  customAttrSurround: ['Arrays of regex\'es that allow to support custom attribute surround expressions (e.g. <input {{#if value}}checked="checked"{{/if}}>)', parseJSONRegExpArray],
  customEventAttributes: ['Arrays of regex\'es that allow to support custom event attributes for minifyJS (e.g. ng-click)', parseJSONRegExpArray],
  decodeEntities: 'Use direct Unicode characters whenever possible',
  html5: 'Parse input according to HTML5 specifications',
  ignoreCustomComments: ['Array of regex\'es that allow to ignore certain comments, when matched', parseJSONRegExpArray],
  ignoreCustomFragments: ['Array of regex\'es that allow to ignore certain fragments, when matched (e.g. <?php ... ?>, {{ ... }})', parseJSONRegExpArray],
  includeAutoGeneratedTags: 'Insert tags generated by HTML parser',
  keepClosingSlash: 'Keep the trailing slash on singleton elements',
  maxLineLength: ['Max line length', parseInt],
  minifyCSS: ['Minify CSS in style elements and style attributes (uses clean-css)', parseJSON],
  minifyJS: ['Minify Javascript in script elements and on* attributes (uses terser)', parseJSON],
  minifyURLs: ['Minify URLs in various attributes (uses relateurl)', parseJSON],
  noNewlinesBeforeTagClose: 'Never add a newline before a tag that closes an element',
  preserveLineBreaks: 'Always collapse to 1 line break (never remove it entirely) when whitespace between tags include a line break.',
  preventAttributesEscaping: 'Prevents the escaping of the values of attributes.',
  processConditionalComments: 'Process contents of conditional comments through minifier',
  processScripts: ['Array of strings corresponding to types of script elements to process through minifier (e.g. "text/ng-template", "text/x-handlebars-template", etc.)', parseJSONArray],
  quoteCharacter: ['Type of quote to use for attribute values (\' or ")', parseString],
  removeAttributeQuotes: 'Remove quotes around attributes when possible.',
  removeComments: 'Strip HTML comments',
  removeEmptyAttributes: 'Remove all attributes with whitespace-only values',
  removeEmptyElements: 'Remove all elements with empty contents',
  removeOptionalTags: 'Remove unrequired tags',
  removeRedundantAttributes: 'Remove attributes when value matches default.',
  removeScriptTypeAttributes: 'Removes the following attributes from script tags: text/javascript, text/ecmascript, text/jscript, application/javascript, application/x-javascript, application/ecmascript. Other type attribute values are left intact',
  removeStyleLinkTypeAttributes: 'Remove type="text/css" from style and link tags. Other type attribute values are left intact.',
  removeTagWhitespace: 'Remove space between attributes whenever possible',
  sortAttributes: 'Sort attributes by frequency',
  sortClassName: 'Sort style classes by frequency',
  trimCustomFragments: 'Trim white space around ignoreCustomFragments.',
  useShortDoctype: 'Replaces the doctype with the short (HTML5) doctype'
};
var mainOptionKeys = Object.keys(mainOptions);
mainOptionKeys.forEach(function(key) {
  var option = mainOptions[key];
  if (Array.isArray(option)) {
    key = key === 'minifyURLs' ? '--minify-urls' : '--' + paramCase(key);
    key += option[1] === parseJSON ? ' [value]' : ' <value>';
    program.option(key, option[0], option[1]);
  }
  else if (~['html5', 'includeAutoGeneratedTags'].indexOf(key)) {
    program.option('--no-' + paramCase(key), option);
  }
  else {
    program.option('--' + paramCase(key), option);
  }
});
program.option('-o --output <file>', 'Specify output file (if not specified STDOUT will be used for output)');

function readFile(file) {
  try {
    return fs.readFileSync(file, { encoding: 'utf8' });
  }
  catch (e) {
    fatal('Cannot read ' + file + '\n' + e.message);
  }
}

var config = {};
program.option('-c --config-file <file>', 'Use config file', function(configPath) {
  var data = readFile(configPath);
  try {
    config = JSON.parse(data);
  }
  catch (je) {
    try {
      config = require(path.resolve(configPath));
    }
    catch (ne) {
      fatal('Cannot read the specified config file.\nAs JSON: ' + je.message + '\nAs module: ' + ne.message);
    }
  }
  mainOptionKeys.forEach(function(key) {
    if (key in config) {
      var option = mainOptions[key];
      if (Array.isArray(option)) {
        var value = config[key];
        config[key] = option[1](typeof value === 'string' ? value : JSON.stringify(value));
      }
    }
  });
});
program.option('--input-dir <dir>', 'Specify an input directory');
program.option('--output-dir <dir>', 'Specify an output directory');
program.option('--file-ext <text>', 'Specify an extension to be read, ex: html');
var content;
program.arguments('[files...]').action(function(files) {
  content = files.map(readFile).join('');
}).parse(process.argv);

const programOptions = program.opts();

function createOptions() {
  var options = {};
  mainOptionKeys.forEach(function(key) {
    var param = programOptions[key === 'minifyURLs' ? 'minifyUrls' : camelCase(key)];
    if (typeof param !== 'undefined') {
      options[key] = param;
    }
    else if (key in config) {
      options[key] = config[key];
    }
  });
  return options;
}

function mkdir(outputDir, callback) {
  fs.mkdir(outputDir, function(err) {
    if (err) {
      switch (err.code) {
        case 'ENOENT':
          return mkdir(path.join(outputDir, '..'), function() {
            mkdir(outputDir, callback);
          });
        case 'EEXIST':
          break;
        default:
          fatal('Cannot create directory ' + outputDir + '\n' + err.message);
      }
    }
    callback();
  });
}

function processFile(inputFile, outputFile) {
  fs.readFile(inputFile, { encoding: 'utf8' }, async function(err, data) {
    if (err) {
      fatal('Cannot read ' + inputFile + '\n' + err.message);
    }
    var minified;
    try {
      minified = await minify(data, createOptions());
    }
    catch (e) {
      fatal('Minification error on ' + inputFile + '\n' + e.message);
    }
    fs.writeFile(outputFile, minified, { encoding: 'utf8' }, function(err) {
      if (err) {
        fatal('Cannot write ' + outputFile + '\n' + err.message);
      }
    });
  });
}

function processDirectory(inputDir, outputDir, fileExt) {
  fs.readdir(inputDir, function(err, files) {
    if (err) {
      fatal('Cannot read directory ' + inputDir + '\n' + err.message);
    }
    files.forEach(function(file) {
      var inputFile = path.join(inputDir, file);
      var outputFile = path.join(outputDir, file);
      fs.stat(inputFile, function(err, stat) {
        if (err) {
          fatal('Cannot read ' + inputFile + '\n' + err.message);
        }
        else if (stat.isDirectory()) {
          processDirectory(inputFile, outputFile, fileExt);
        }
        else if (!fileExt || path.extname(file) === '.' + fileExt) {
          mkdir(outputDir, function() {
            processFile(inputFile, outputFile);
          });
        }
      });
    });
  });
}

async function writeMinify() {
  var minified;
  try {
    minified = await minify(content, createOptions());
  }
  catch (e) {
    fatal('Minification error:\n' + e.message);
  }
  (programOptions.output ? fs.createWriteStream(programOptions.output).on('error', function(e) {
    fatal('Cannot write ' + programOptions.output + '\n' + e.message);
  }) : process.stdout).write(minified);
}

var inputDir = programOptions.inputDir;
var outputDir = programOptions.outputDir;
var fileExt = programOptions.fileExt;
if (inputDir || outputDir) {
  if (!inputDir) {
    fatal('The option output-dir needs to be used with the option input-dir. If you are working with a single file, use -o.');
  }
  else if (!outputDir) {
    fatal('You need to specify where to write the output files with the option --output-dir');
  }
  processDirectory(inputDir, outputDir, fileExt);
}
// Minifying one or more files specified on the CMD line
else if (content) {
  writeMinify();
}
// Minifying input coming from STDIN
else {
  content = '';
  process.stdin.setEncoding('utf8');
  process.stdin.on('data', function(data) {
    content += data;
  }).on('end', writeMinify);
}


html-minifier-terser

description: Highly configurable, well-tested, JavaScript-based HTML minifier.

author: Daniel Ruf

maintainers: Juriy Zaytsev <kangax@gmail.com> (http://perfectionkills.com/)

keywords: cli compress compressor css html htmlmin javascript min minification minifier minify optimize optimizer pack packer parse parser terser uglifier uglify

dist: grunt dist

test: grunt test

prepare: is-ci || husky install

serve: npm run dist && serve . -p 6753