@onesy/lz77
Version:
242 lines (241 loc) • 12 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const is_1 = __importDefault(require("@onesy/utils/is"));
const isEnvironment_1 = __importDefault(require("@onesy/utils/isEnvironment"));
const to_1 = __importDefault(require("@onesy/utils/to"));
const castParam_1 = __importDefault(require("@onesy/utils/castParam"));
const OnesyDate_1 = __importDefault(require("@onesy/date/OnesyDate"));
const duration_1 = __importDefault(require("@onesy/date/duration"));
class OnesyLZ77Response {
constructor(value = '', original_byte_size, value_byte_size, compression_ratio, compression_percentage, positive, performance_milliseconds, performance) {
this.value = value;
this.original_byte_size = original_byte_size;
this.value_byte_size = value_byte_size;
this.compression_ratio = compression_ratio;
this.compression_percentage = compression_percentage;
this.positive = positive;
this.performance_milliseconds = performance_milliseconds;
this.performance = performance;
}
}
class OnesyLZ77 {
constructor(value) {
this.value = value;
this.response = new OnesyLZ77Response();
if (this.value !== undefined)
this.init();
}
static get OnesyLZ77Response() { return OnesyLZ77Response; }
static decode(value) {
return new OnesyLZ77().decode(value);
}
static prefix(value) {
if (!(0, is_1.default)('string', value))
return '`';
const special = ['`', `'`, `"`, '(', ')', '|', ',', '+', '*', '-', '.', '^', ';', ':', '=', '?', '[', ']', '_'];
let index = 0;
let amount = 1;
let prefix = '';
while (true) {
prefix = special[index].repeat(amount);
const regexp = new RegExp(`\\${prefix}[^\\${prefix}]{1,4},[^\\${prefix}]{1,4}\\${prefix}`);
const match = value.match(regexp);
if (!match)
break;
// Increment
if (index === special.length - 1) {
index = 0;
amount++;
}
else
index++;
}
return prefix;
}
get encoded() {
return this.response;
}
init() {
if (['uint8array', 'buffer', 'string'].some((item) => (0, is_1.default)(item, this.value))) {
this.getVariant();
if ((0, is_1.default)('string', this.value)) {
this.valueString = this.value;
this.valueEncoded = new TextEncoder().encode(this.value);
}
else
this.valueEncoded = this.value;
if ((0, is_1.default)('buffer', this.value))
this.valueString = this.value.toString('utf-8');
if ((0, is_1.default)('uint8array', this.value))
this.valueString = new TextDecoder().decode(this.value);
// Encode
this.encode();
}
}
encode() {
const startTime = OnesyDate_1.default.milliseconds;
const prefix = OnesyLZ77.prefix(this.valueString);
const windowLength = 32e4;
const searchIndexes = { start: -1, end: 0 };
let search = this.valueEncoded.subarray(searchIndexes.start, searchIndexes.end);
const lookaheadIndexes = { start: 0, end: this.valueEncoded.length > windowLength ? windowLength : this.valueEncoded.length };
let lookahead = this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.end);
let output = [];
const getItem = () => {
const allFirstMatches = [];
for (let i = searchIndexes.end - 1; i > -1; i--) {
if (this.valueEncoded[i] === lookahead[0])
allFirstMatches.push(i);
}
if (!allFirstMatches.length)
return [0, 0, lookaheadIndexes.start, lookaheadIndexes.start];
let matchLongest = [];
allFirstMatches.sort();
for (let i = 0; i < allFirstMatches.length; i++) {
const start = allFirstMatches[i];
let offset = 2;
let equal = this.equal(this.valueEncoded.subarray(start, start + offset), this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.start + offset));
while (equal) {
offset += 1;
equal = this.equal(this.valueEncoded.subarray(start, start + offset), this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.start + offset));
}
if (!matchLongest.length || (offset >= matchLongest[1]))
matchLongest = [start, offset];
}
return [lookaheadIndexes.start - matchLongest[0], matchLongest[1] - 1, lookaheadIndexes.start, lookaheadIndexes.start + matchLongest[1] - 1];
};
while (lookahead.length) {
const [start, offset, lookaheadStartIndex, lookaheadEndIndex] = getItem();
const diff = Math.abs(start - offset);
let value = '';
const valueToAppend = this.valueEncoded[lookaheadEndIndex];
const actualValue = this.valueEncoded.subarray(lookaheadStartIndex - start, lookaheadStartIndex - start + offset);
if (!(start === 0 && offset === 0))
value = `${prefix}${start === offset ? this.valueEncode(start) : `${this.valueEncode(start)},${this.valueEncode(offset)}`}${prefix}`;
output.push(...((0, to_1.default)(actualValue.reduce((result, item) => result += String.fromCharCode(item), ''), 'byte-size') <= (0, to_1.default)(value, 'byte-size') ? actualValue : [value]), valueToAppend);
const move = lookaheadEndIndex - lookaheadStartIndex + diff;
searchIndexes.start = search.length + lookahead.length >= windowLength ? searchIndexes.start + move : 0;
searchIndexes.end = lookaheadEndIndex + 1;
lookaheadIndexes.start = searchIndexes.end;
lookaheadIndexes.end += move;
search = this.valueEncoded.subarray(searchIndexes.start, searchIndexes.end);
lookahead = this.valueEncoded.subarray(lookaheadIndexes.start, lookaheadIndexes.end);
}
output = output.map((item) => (0, is_1.default)('number', item) ? String.fromCharCode(item) : item).join('');
output = ` ${output}`;
const variantCodeValue = this.variantToValue();
if (variantCodeValue > 0 || prefix !== '`') {
if (prefix !== '`')
output = `${prefix}${output}`;
if (variantCodeValue > 0)
output = `${variantCodeValue}${output}`;
}
const response = new OnesyLZ77Response(output);
response.performance_milliseconds = OnesyDate_1.default.milliseconds - startTime;
response.performance = (0, duration_1.default)(response.performance_milliseconds) || '0 milliseconds';
response.original_byte_size = (0, to_1.default)(this.valueEncoded, 'byte-size');
response.value_byte_size = (0, to_1.default)(response.value, 'byte-size');
response.compression_ratio = Number((((response.value_byte_size + response.original_byte_size) / response.value_byte_size) - 1).toFixed(2));
response.compression_percentage = response.original_byte_size === 0 ? response.value_byte_size === 0 ? 0 : response.value_byte_size * -100 : Number(((((response.original_byte_size - response.value_byte_size) / response.original_byte_size)) * 100).toFixed(2));
response.positive = response.compression_ratio > 1;
this.response = response;
return response;
}
decode(value_) {
const response = new OnesyLZ77Response();
const startTime = OnesyDate_1.default.milliseconds;
if ((0, is_1.default)('string', value_)) {
let meta = '';
let value = value_;
const indexSeparator = value_.indexOf(' ');
meta = value_.substring(0, indexSeparator);
value = value_.substring(indexSeparator + 1);
let variant = '0';
let prefix = '`';
if (['1', '2'].indexOf(meta[0]) > -1) {
variant = meta[0];
meta = meta.substring(1);
}
if (meta.length)
prefix = meta;
const regexp = new RegExp(`\\${prefix}[^\\${prefix}]{1,4}(,[^\\${prefix}]{1,4})?\\${prefix}`, 'g');
const abbrs = value.match(regexp) || [];
for (const abbr of abbrs) {
let valueAbbr = abbr;
if (!valueAbbr.includes(','))
valueAbbr = `${prefix}${valueAbbr.replace(new RegExp(`${prefix}`, 'g'), '')},${valueAbbr.replace(new RegExp(`${prefix}`, 'g'), '')}${prefix}`;
let [position, offset] = valueAbbr.slice(1, -1).split(',');
[position, offset] = [position, offset].map(item => this.valueDecode(item));
const index = value.indexOf(abbr);
const addStartIndex = index - position;
const addEndIndex = addStartIndex + offset;
const array = [];
if (offset > position) {
let left = offset;
while (left > 0) {
const add = value.substring(addStartIndex, left < position ? addStartIndex + left : addStartIndex + position);
array.push(add);
left -= position;
}
}
else {
const add = value.substring(addStartIndex, addEndIndex);
array.push(add);
}
const itemValue = array.join('');
value = value.replace(abbr, itemValue);
}
value = this.valueToVariant(value, variant);
// Convert value back from UTF-16 -> Utf8Array -> Text decoded
// ie. back to Japanese characters
if ((0, is_1.default)('string', value)) {
const array = [];
value.split('').forEach(item => array.push(item.charCodeAt(0)));
value = new TextDecoder().decode(new Uint8Array(array));
}
response.value = value;
response.performance_milliseconds = OnesyDate_1.default.milliseconds - startTime;
response.performance = (0, duration_1.default)(response.performance_milliseconds) || '0 milliseconds';
response.original_byte_size = (0, to_1.default)(value, 'byte-size');
response.value_byte_size = (0, to_1.default)(value_, 'byte-size');
}
return response;
}
valueToVariant(value, variant_) {
const variant = (0, castParam_1.default)(variant_);
if ((0, isEnvironment_1.default)('nodejs') && variant === 1)
return Buffer.from(value);
if (((0, isEnvironment_1.default)('browser') && variant === 1) || variant === 2)
return new TextEncoder().encode(value);
return value;
}
variantToValue() {
let value = 0;
if ((0, isEnvironment_1.default)('nodejs') && this.variant === Buffer)
value = 1;
else if (this.variant === Uint8Array)
value = 2;
return value;
}
valueEncode(value) {
return Number(value).toString(36);
}
valueDecode(value) {
return parseInt(value, 36);
}
getVariant() {
if ((0, isEnvironment_1.default)('nodejs') && Buffer.isBuffer(this.value))
this.variant = Buffer;
else if ((0, is_1.default)('uint8array', this.value))
this.variant = Uint8Array;
else
this.variant = String;
}
equal(value, value1) {
return value.join('') === value1.join('');
}
}
exports.default = OnesyLZ77;