Demonstrating the RSA Algorithm in JavaScript

The JavaScript code below picks two random two-digit primes, uses them to generate a RSA keypair, then uses that keypair to encrypt and decrypt a simple message. It uses the LargeInteger class introduced in a previous post to handle the very large numbers involved in the RSA encryption process. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

RSA is an asymmetric encryption algorithm developed in the late 1970’s by the researchers Ron Rivest, Adi Shamir, and Leonard Adleman. It works by the process described below.

1. Choose two (ideally very large) prime numbers at random. Call these numbers p and q.

2. Multiply p and q together to find the modulus.

3. Find the least common multiple of (p – 1) and (q – 1). Call this value the totient.

4. Choose a random number greater than 2 and less than the totient. Call this the public exponent candidate.

5. Verify that the public exponent candidate and the totient are co-prime, that is to say, that they have no common factors other than 1. If they are not co-prime, return to step 4 and choose another candidate. If they are co-prime, accept the candidate as the public exponent.

6. Find the modular multiplicative inverse of the public exponent with respect to the modulus, by way of an algorithm too complicated to describe here. This value is the private key.

7. To encrypt a message, first encode it as a number or a series of numbers (each less than the modulus?), raise each number to the public exponent, and “modulo” it against the modulus (that is, divide it by the modulus and take the remainder). This is the encrypted value.

8. To decrypt an encrypted value, raise it to the private exponent and modulo it against the modulus again. The result should be the original, unecrypted value.

This program is for demonstration purposes only, and should be no means be depended on to secure anything in the real world. Aside from any other number of known and unknown vulnerabilities, an RSA keypair that uses two-digit primes obviously can’t possibly be very secure, but that’s all this implementation can handle due to the inefficiency of its various mathematical calculations.

RSAEncryption.png


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var newline = "<br />";

	// This implementation can't handle big numbers.
	var digitsPerPrime = 2; 

	var keyPair = new EncryptionKeyPair().generate(digitsPerPrime);
	document.write("Key pair is " + keyPair.toString() + newline);

	// hack
	// We're assuming the modulus is greater than the messageToEncrypt.
	// I believe this may cause problems on the rare occasions 
	// when the modulus ends up being less than the message value.

	var messageToEncryptAsInt = 42;
	var base = keyPair.modulus.base;
	var messageToEncrypt = new LargeInteger(base).setFromInt(messageToEncryptAsInt);
	document.write("Message to encrypt is " + messageToEncrypt.toString() + newline);
	
	var messageEncrypted = keyPair.encrypt(messageToEncrypt);
	document.write("Message encrypted is " + messageEncrypted.toString() + newline);

	var messageDecrypted = keyPair.decrypt(messageEncrypted);
	document.write("Message decrypted is " + messageDecrypted.toString() + newline);
}

// classes

function EncryptionKeyPair(modulus, publicExponent, privateExponent)
{
	this.modulus = modulus;
	this.publicExponent = publicExponent;
	this.privateExponent = privateExponent;
}
{
	EncryptionKeyPair.prototype.decrypt = function(messageToDecrypt)
	{
		return this.encryptOrDecrypt(messageToDecrypt, this.publicExponent);	
	}

	EncryptionKeyPair.prototype.encrypt = function(messageToEncrypt)
	{
		return this.encryptOrDecrypt(messageToEncrypt, this.privateExponent);	
	}

	EncryptionKeyPair.prototype.encryptOrDecrypt = function(message, exponent)
	{
		var result = message.raiseToPower
		(
			exponent
		).modulo
		(
			this.modulus
		);

		return message;
	}

	EncryptionKeyPair.prototype.generate = function(digitsPerPrime)
	{
		// Adapted from an example found at the URL
		// https://en.wikipedia.org/wiki/RSA_(cryptosystem)

		var primesRandom = [];
		var base = 10;
		var valueMaxForNumberOfDigits = 
			new LargeInteger(base).setFromInt(base).raiseToPower
			(
				new LargeInteger(base).setFromInt(digitsPerPrime)
			);

		var numberOfPrimesToChoose = 2;

		for (var i = 0; i < numberOfPrimesToChoose; i++)
		{
			var primeCandidate = 
				valueMaxForNumberOfDigits.clone().randomize();

			while (MathHelperLargeInteger.isPrime(primeCandidate) == false)
			{
				primeCandidate.increment();
			}

			primesRandom.push(primeCandidate);
		}
				
		var prime0 = primesRandom[0];
		var prime1 = primesRandom[1];

		var modulus = prime0.clone().multiply(prime1);

		var totient = MathHelperLargeInteger.leastCommonMultiple
		(
			prime0.clone().decrement(), 
			prime1.clone().decrement()
		);

		var publicExponentCandidate = totient.clone().randomize();

		var areCoprime = MathHelperLargeInteger.areCoprime;

		while (areCoprime(publicExponentCandidate, totient) == false)
		{
			publicExponentCandidate.increment();
			if (publicExponentCandidate.isGreaterThanOrEqualTo(totient))
			{
				publicExponentCandidate.setFromInt(2);
			}
		}

		var publicExponent = publicExponentCandidate;

		var privateExponent = MathHelperLargeInteger.modularMultiplicativeInverse
		(
			publicExponent,
			totient
		);
		
		this.modulus = modulus;
		this.publicExponent = publicExponent;
		this.privateExponent = privateExponent;

		return this;
	}

	// string 

	EncryptionKeyPair.prototype.toString = function()
	{
		var objectToSerialize = 
		{
			"modulus" : this.modulus.toString(),
			"publicExponent" : this.publicExponent.toString(),
			"privateExponent" : this.privateExponent.toString()

		}
		var returnValue = JSON.stringify(objectToSerialize);
		return returnValue;
	}
}

function LargeInteger(base)
{
	this.base = base;
	this.digits = [];
}
{
	// instance methods

	LargeInteger.prototype.add = function(other)
	{
		var numberOfDigitsInThis = this.digits.length;
		var numberOfDigitsInOther = other.digits.length;

		var numberOfDigitsGreater;

		if (numberOfDigitsInThis >= numberOfDigitsInOther)
		{
			numberOfDigitsGreater = numberOfDigitsInThis;
		}
		else
		{
			numberOfDigitsGreater = numberOfDigitsInOther;
		}

		var numberOfDigitsInSum = numberOfDigitsGreater + 1;

		this.expandNumberOfDigitsTo(numberOfDigitsInSum);
		other.expandNumberOfDigitsTo(numberOfDigitsInSum);

		var carryDigit = 0;

		for (var i = 0; i < numberOfDigitsInSum; i++)
		{
			var sumAtDigit = this.digits[i] + other.digits[i] + carryDigit;

			var digitValue = sumAtDigit % this.base;
			carryDigit = (sumAtDigit - digitValue) / this.base;

			this.digits[i] = digitValue;
		}

		this.removeLeadingZeroes();
		other.removeLeadingZeroes();

		return this;
	}

	LargeInteger.prototype.clone = function()
	{
		var returnValue = new LargeInteger(this.base);

		returnValue.overwriteWith(this);

		return returnValue;
	}

	LargeInteger.prototype.decrement = function()
	{
		return this.subtract(new LargeInteger(this.base).setFromInt(1));
	}

	LargeInteger.prototype.divide = function(other)
	{
		var dividend = this.clone();
		var divisor = other.clone();
		var base = dividend.base;

		var one = new LargeInteger(base).setFromInt(1);

		var lengthOfDivisorInDigits = divisor.digits.length;
		var differenceInLengths = dividend.digits.length - lengthOfDivisorInDigits;

		dividend.multiplyByBaseRaisedTo(lengthOfDivisorInDigits);
		divisor.multiplyByBaseRaisedTo(differenceInLengths + lengthOfDivisorInDigits);

		var result = new LargeInteger(base).setFromInt(0);

		while (divisor.digits.length > 0)
		{
			if (divisor.isLessThanOrEqualTo(dividend))
			{
				dividend.subtract(divisor);
				result.add(one);
			}
			else
			{
				divisor.divideByBaseRaisedTo(1);
				result.multiplyByBaseRaisedTo(1);
			}	
		}

		result.divideByBaseRaisedTo(2 * lengthOfDivisorInDigits);

		this.overwriteWith(result);

		return this;
	}

	LargeInteger.prototype.divideByBaseRaisedTo = function(exponent)
	{
		this.digits.splice(0, exponent);

		return this;
	}

	LargeInteger.prototype.expandNumberOfDigitsTo = function(numberOfDigitsTotal)
	{
		var numberOfDigitsToAdd = numberOfDigitsTotal - this.digits.length;
		for (var i = 0; i < numberOfDigitsToAdd; i++)
		{
			this.digits.push(0);	
		}

		return this;
	}

	LargeInteger.prototype.increment = function()
	{
		return this.add(new LargeInteger(this.base).setFromInt(1));
	}

	LargeInteger.prototype.isEqualTo = function(other)
	{
		var returnValue;
		var numberOfDigitsInThis = this.digits.length;
		var numberOfDigitsInOther = other.digits.length;

		if (numberOfDigitsInThis != numberOfDigitsInOther)
		{
			returnValue = false;
		}
		else
		{
			returnValue = true;

			for (var i = numberOfDigitsInThis - 1; i >= 0; i--)
			{
				var digitThis = this.digits[i];
				var digitOther = other.digits[i];

				if (digitThis != digitOther)
				{
					returnValue = false;
					break;
				}
			}
		}

		return returnValue;
	}

	LargeInteger.prototype.isGreaterThan = function(other)
	{
		var returnValue = (this.isLessThanOrEqualTo(other) == false);
		return returnValue;
	}

	LargeInteger.prototype.isGreaterThanOrEqualTo = function(other)
	{
		var returnValue;
		var numberOfDigitsInThis = this.digits.length;
		var numberOfDigitsInOther = other.digits.length;

		if (numberOfDigitsInThis > numberOfDigitsInOther)
		{
			returnValue = true;
		}
		else if (numberOfDigitsInThis == numberOfDigitsInOther)
		{
			returnValue = true;

			for (var i = numberOfDigitsInThis - 1; i >= 0; i--)
			{
				var digitThis = this.digits[i];
				var digitOther = other.digits[i];

				if (digitThis < digitOther)
				{
					returnValue = false;
					break;
				}
				else if (digitThis > digitOther)
				{
					break;
				}			
			}
		}
		else
		{
			returnValue = false;
		}

		return returnValue;
	}

	LargeInteger.prototype.isLessThan = function(other)
	{
		var returnValue = (this.isGreaterThanOrEqualTo(other) == false);
		return returnValue;
	}

	LargeInteger.prototype.isLessThanOrEqualTo = function(other)
	{
		var returnValue;
		var numberOfDigitsInThis = this.digits.length;
		var numberOfDigitsInOther = other.digits.length;

		if (numberOfDigitsInThis < numberOfDigitsInOther)
		{
			returnValue = true;
		}
		else if (numberOfDigitsInThis == numberOfDigitsInOther)
		{
			returnValue = true;

			for (var i = numberOfDigitsInThis - 1; i >= 0; i--)
			{
				var digitThis = this.digits[i];
				var digitOther = other.digits[i];

				if (digitThis > digitOther)
				{
					returnValue = false;
					break;
				}
				else if (digitThis < digitOther)
				{
					break;
				}			
			}
		}
		else
		{
			returnValue = false;
		}

		return returnValue;
	}

	LargeInteger.prototype.isNotEqualTo = function(other)
	{
		var returnValue = (this.isEqualTo(other) == false);
		return returnValue;
	}

	LargeInteger.prototype.modulo = function(other)
	{
		var dividend = this.clone();
		var divisor = other.clone();

		var lengthOfDivisorInDigits = divisor.digits.length;
		var differenceInLengths = dividend.digits.length - lengthOfDivisorInDigits;

		var divisorOriginal = divisor.clone();
		divisor.multiplyByBaseRaisedTo(differenceInLengths);

		while (divisor.digits.length >= lengthOfDivisorInDigits)
		{
			if (divisor.isLessThanOrEqualTo(dividend))
			{
				dividend.subtract(divisor);
			}
			else
			{
				divisor.divideByBaseRaisedTo(1);
			}	
		}

		this.overwriteWith(dividend);

		return this;
	}

	LargeInteger.prototype.multiply = function(other)
	{
		var numberOfDigitsInThis = this.digits.length;
		var numberOfDigitsInOther = other.digits.length;		

		var numberOfDigitsInProduct = numberOfDigitsInThis + numberOfDigitsInOther; 
		var product = new LargeInteger(this.base).expandNumberOfDigitsTo(numberOfDigitsInProduct);

		for (var i = 0; i < numberOfDigitsInThis; i++)
		{
			var digitFromThis = this.digits[i];

			for (var j = 0; j < numberOfDigitsInOther; j++)
			{
				var digitFromOther = other.digits[j];

				var productOfDigits = 
					digitFromThis 
					* digitFromOther;

				var productDigitIndex = i + j;

				var carryDigit = productOfDigits; 

				while (carryDigit > 0)
				{
					var sumAtDigit = product.digits[productDigitIndex] + carryDigit;

					var digitValue = sumAtDigit % this.base;
					carryDigit = (sumAtDigit - digitValue) / this.base;

					product.digits[productDigitIndex] = digitValue;

					productDigitIndex++;
				}
			}
		}

		product.removeLeadingZeroes();
		this.overwriteWith(product);

		return this;
	}

	LargeInteger.prototype.multiplyByBaseRaisedTo = function(exponent)
	{
		for (var i = 0; i < exponent; i++)
		{
			this.digits.splice(0, 0, 0);
		}

		return this;
	}

	LargeInteger.prototype.overwriteWith = function(other)
	{
		var numberOfDigitsInThis = this.digits.length;
		var numberOfDigitsInOther = other.digits.length;

		for (var i = 0; i < numberOfDigitsInOther; i++)
		{
			this.digits[i] = other.digits[i];
		}

		if (numberOfDigitsInThis > numberOfDigitsInOther)
		{
			this.digits.splice
			(
				numberOfDigitsInOther, 
				numberOfDigitsInThis - numberOfDigitsInOther
			);
		}		

		return this;
	}

	LargeInteger.prototype.randomize = function()
	{
		var valueOriginal = this.clone();
		var base = new LargeInteger(this.base).setFromInt(this.base);
		var numberOfDigits = new LargeInteger(this.base).setFromInt(this.digits.length);		
		var valueMaxForNumberOfDigits = base.raiseToPower(numberOfDigits);

		for (var i = 0; i < this.digits.length; i++)
		{
			digitRandom = Math.floor
			(
				Math.random() * this.base
			);

			this.digits[i] = digitRandom;
		}

		this.multiply(valueOriginal).divide(valueMaxForNumberOfDigits);

		return this;
	}

	LargeInteger.prototype.raiseToPower = function(exponent)
	{
		var result = this.raiseToPowerBySquaring(exponent);

		this.overwriteWith(result);

		return this;
	}

	LargeInteger.prototype.raiseToPowerBySquaring = function(exponent)
	{
		var returnValue = LargeInteger.raiseValueToPowerBySquaring
		(
			this.clone(), 
			exponent.clone(), 
			new LargeInteger(this.base).setFromInt(1),
			new LargeInteger(this.base).setFromInt(2)
		);

		return returnValue;
	}

	LargeInteger.raiseValueToPowerBySquaring = function
	(
		valueToRaise, exponent, constantOne, constantTwo
	)
	{
		var returnValue;

		if (exponent.digits.length == 1 && exponent.digits[0] == 1)
		{
			returnValue = valueToRaise;
		}
		else if (exponent.digits[0] % 2 == 0)
		{
			returnValue = LargeInteger.raiseValueToPowerBySquaring
			(
				valueToRaise.clone().multiply(valueToRaise), 
				exponent.clone().divide(constantTwo),
				constantOne,
				constantTwo
			);
		}
		else
		{
			returnValue = LargeInteger.raiseValueToPowerBySquaring
			(
				valueToRaise.clone().multiply(valueToRaise), 
				exponent.clone().subtract(constantOne).divide(constantTwo),
				constantOne,
				constantTwo
			).multiply
			(
				valueToRaise
			);
		}

		return returnValue;
	}

	LargeInteger.prototype.removeLeadingZeroes = function()
	{
		return this.removeLeadingZeroesDownTo(0);
	}

	LargeInteger.prototype.removeLeadingZeroesDownTo = function(numberOfDigitsTotal)
	{
		var i = this.digits.length - 1;

		while (i >= numberOfDigitsTotal && this.digits[i] == 0)
		{
			this.digits.splice(i, 1);	
			i--;
		}

		return this;
	}

	LargeInteger.prototype.setFromInt = function(valueToSet)
	{
		var d = 0;

		while (valueToSet > 0)
		{
			var digitValue = valueToSet % this.base;
			valueToSet = (valueToSet - digitValue) / this.base;
			this.digits[d] = digitValue;

			d++;
		}

		this.removeLeadingZeroes();

		return this;
	}

	LargeInteger.prototype.setToOne = function()
	{
		this.digits = [1];
		return this;
	}

	LargeInteger.prototype.setToZero = function()
	{
		this.digits = [];
		return this;
	}

	LargeInteger.prototype.subtract = function(other)
	{
		var base = this.base;

		var numberOfDigitsInOther = other.digits.length;

		var numberOfDigitsInThis = this.digits.length;
		other.expandNumberOfDigitsTo(numberOfDigitsInThis);

		for (var i = 0; i < numberOfDigitsInOther; i++)
		{
			var digitFromThis = this.digits[i];
			var digitFromOther = other.digits[i];

			if (digitFromThis < digitFromOther)
			{
				var valueBorrowed = 0;
				var j = i;

				while (valueBorrowed == 0)
				{
					j++;

					var digitToBorrowFrom = this.digits[j];
					if (digitToBorrowFrom > 0)
					{
						valueBorrowed = base;
						this.digits[j]--;

						j--;
						while (j > i)
						{
							this.digits[j] = base - 1;
							j--;
						}
					}
				}

				digitFromThis += valueBorrowed;				
			}

			this.digits[i] = digitFromThis - digitFromOther;
		}

		this.removeLeadingZeroes();
		other.removeLeadingZeroes();

		return this;
	}

	LargeInteger.prototype.toInt = function()
	{
		var returnValue = 0;

		var placeMultiplierCurrent = 1;

		var numberOfDigits = this.digits.length;

		for (var i = 0; i < numberOfDigits; i++)
		{
			returnValue += this.digits[i] * placeMultiplierCurrent;

			placeMultiplierCurrent *= this.base;
		}

		return returnValue;
	}


	LargeInteger.prototype.toString = function()
	{
		var returnValue = "";

		var numberOfDigits = this.digits.length;

		for (var i = numberOfDigits - 1; i >= 0; i--)
		{
			returnValue += "" + this.digits[i];
		}

		return returnValue;
	}
}

function MathHelperLargeInteger()
{
	// static class
}
{
	MathHelperLargeInteger.areCoprime = function(a, b)
	{
		var greatestCommonDivisor = MathHelperLargeInteger.greatestCommonDivisor(a, b);
		var one = new LargeInteger(a.base).setToOne();
		var returnValue = greatestCommonDivisor.isEqualTo(one);
		return returnValue;
	}

	MathHelperLargeInteger.greatestCommonDivisor = function(a, b)
	{
		// Adapted from pseudocode at the URL:
		// https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm

		var base = a.base;

		var s = new LargeInteger(base).setToZero();    
		var old_s = new LargeInteger(base).setToOne();

		var t = new LargeInteger(base).setToOne();   
		var old_t = new LargeInteger(base).setToZero();

		var r = b.clone();
		var old_r = a.clone();

		var quotient = new LargeInteger(base);
		var temp = new LargeInteger(base);
		var temp2 = new LargeInteger(base);
		var zero = new LargeInteger(base).setToZero();
		
		while (r.isNotEqualTo(zero))
		{
			quotient.overwriteWith(old_r).divide(r);

			temp.overwriteWith(r);
			r.overwriteWith(old_r).subtract
			(
				temp2.overwriteWith(quotient).multiply(temp)
			);
			old_r.overwriteWith(temp);

			temp.overwriteWith(s);
			s.overwriteWith(old_s).subtract
			(
				temp2.overwriteWith(quotient).multiply(temp)
			);
			old_s.overwriteWith(temp);

			temp.overwriteWith(t);
			t.overwriteWith(old_t).subtract
			(
				temp2.overwriteWith(quotient).multiply(temp)
			);
			old_t.overwriteWith(temp);
		}

		var returnValue = old_r;
	
		return returnValue;
	}

	MathHelperLargeInteger.isPrime = function(possiblePrime, two, zero, temp)
	{
		// hack - This is almost certainly not the most efficient algorithm.

		var returnValue = true;

		if (two == null)
		{
			var base = possiblePrime.base;
			two = new LargeInteger(base).setFromInt(2);
			zero = new LargeInteger(base).setToZero();
			temp = new LargeInteger(base);
		}
		
		if (possiblePrime.isLessThan(two))
		{
			returnValue = false;	
		}
		else
		{
			// hack - A lot of re-instancing here.
			var possiblePrimeHalf = possiblePrime.clone().divide(two);
			var factorCurrent = two.clone();

			while (factorCurrent.isLessThanOrEqualTo(possiblePrimeHalf))
			{
				while (MathHelperLargeInteger.isPrime(factorCurrent, two, zero, temp) == false)
				{
					factorCurrent.increment();
				}

				var isPossiblePrimeAMultipleOfCurrentDivisor = 
					temp.overwriteWith
					(
						possiblePrime
					).modulo
					(
						factorCurrent
					).isEqualTo
					(
						zero
					);
	
				if (isPossiblePrimeAMultipleOfCurrentDivisor == true)
				{
					returnValue = false;
					break;
				}

				factorCurrent.increment();
			}
		}

		return returnValue;
	}

	MathHelperLargeInteger.leastCommonMultiple = function(a, b)
	{
		var product = a.clone().multiply(b);
		var greatestCommonDivisor = MathHelperLargeInteger.greatestCommonDivisor(a, b);
		var returnValue = product.divide(greatestCommonDivisor);
		return returnValue;
	}

	MathHelperLargeInteger.modularMultiplicativeInverse = function(valueToInvert, modulus)
	{
		// Adapted from:
		// https://math.stackexchange.com/questions/67171/
		// calculating-the-modular-multiplicative-inverse-without-all-those-strange-looking

		var returnValue;

		var base = valueToInvert.base;

		var one = new LargeInteger(base).setToOne();

		if (valueToInvert.isEqualTo(one)) 
		{
			returnValue = one.clone();	
		}
		else
		{
			var temp = new LargeInteger(base);
			var temp2 = new LargeInteger(base);

			var c = modulus.clone().modulo(valueToInvert);
			var d = valueToInvert.clone();
			var u = modulus.clone().divide(valueToInvert);
			var v = one.clone();

			while (c.isNotEqualTo(one) && d.isNotEqualTo(one)) 
			{
				v.add
				(
					temp.overwriteWith
					(
						d
					).divide
					(
						c
					).multiply
					(
						u
					)
				);

				d.modulo(c);

				if (d.isNotEqualTo(one)) 
				{
					u.add
					(
						temp.overwriteWith
						(
							c
						).divide
						(
							d
						).multiply
						(
							v
						)
					);

					c.modulo(d);
				}
			}

			var tAsInt = (d.isNotEqualTo(one) ? 1 : 0);
			var t = new LargeInteger(base).setFromInt(tAsInt);

			returnValue = v.multiply
			(
				temp.overwriteWith
				(
					one
				).subtract
				(
					t
				)
			).add
			(
				temp.overwriteWith(t).multiply
				(
					temp2.overwriteWith
					(
						modulus
					).subtract
					(
						u
					)
				)
			);
		}

		return returnValue;
	}

}

// run

main();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , , , | Leave a comment

A Sliding Tile Puzzle in JavaScript

The code below implements a simple sliding tile puzzle in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

Use the arrow keys to move the cursor around and the Enter key to swap the empty cell with the selected neighbor. The goal is to put the tiles in the correct order from their randomized starting positions, though currently the program doesn’t detect the victory condition anyway.

SlidingTilePuzzle.png


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var display = new Display
	(
		new Coords(100, 100), // sizeInPixels
		10, // fontHeightInPixels
		"White", // colorBack
		"Gray" // colorFore
	);

	var grid = new Grid
	(
		new Coords(4, 4)
	).randomize();

	Globals.Instance.initialize
	(
		display, grid
	);
}

// classes

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

	Coords.prototype.divide = function(other)
	{
		this.x /= other.x;
		this.y /= other.y;
		return this;
	}

	Coords.prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;
		return this;
	}

	Coords.prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

	Coords.prototype.magnitude = function()
	{
		return Math.sqrt(this.x * this.x + this.y * this.y);
	}

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

	Coords.prototype.subtract = function(other)
	{
		this.x -= other.x;
		this.y -= other.y;
		return this;
	}

	Coords.prototype.trimToRangeMax = function(max)
	{
		if (this.x < 0)
		{
			this.x = 0;
		}
		else if (this.x > max.x)
		{
			this.x = max.x;
		}

		if (this.y < 0)
		{
			this.y = 0;
		}
		else if (this.y > max.y)
		{
			this.y = max.y;
		}

		return this;
	}
}

function Display(sizeInPixels, fontHeightInPixels, colorBack, colorFore)
{
	this.sizeInPixels = sizeInPixels;
	this.fontHeightInPixels = fontHeightInPixels;
	this.colorBack = colorBack;
	this.colorFore = colorFore;
}
{
	Display.prototype.initialize = function()
	{
		var canvas = document.createElement("canvas");
		canvas.width = this.sizeInPixels.x;
		canvas.height = this.sizeInPixels.y;

		this.graphics = canvas.getContext("2d");
		this.graphics.font = this.fontHeightInPixels + "px sans-serif";

		document.body.appendChild(canvas);
	}

	// drawing

	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			new Coords(0, 0), 
			this.sizeInPixels, 
			this.colorBack, 
			this.colorFore
		);
	}

	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect
			(
				pos.x, pos.y,
				size.x, size.y
			);
		}
	}

	Display.prototype.drawText = function(text, pos, color)
	{
		this.graphics.fillStyle = color;
		this.graphics.fillText
		(
			text, 
			pos.x, pos.y + this.fontHeightInPixels
		);
	}

}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();
	
	Globals.prototype.initialize = function(display, grid)
	{
		this.display = display;
		this.grid = grid;

		this.display.initialize();
		this.grid.drawToDisplay(this.display);

		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();
	}
}

function Grid(sizeInCells)
{
	this.sizeInCells = sizeInCells;

	this.cells = [];

	this.cursorPos = new Coords(0, 0);
}
{
	Grid.prototype.cellAtPosGet = function(cellPos)
	{
		var cellIndex = this.indexOfCellAtPos(cellPos);
		var cellValue = this.cells[cellIndex];
		return cellValue
	}

	Grid.prototype.cellAtPosSet = function(cellPos, valueToSet)
	{
		var cellIndex = this.indexOfCellAtPos(cellPos);
		this.cells[cellIndex] = valueToSet;
	}

	Grid.prototype.cursorMove = function(direction)
	{
		this.cursorPos.add
		(
			direction
		).trimToRangeMax
		(
			this.sizeInCells
		);
	}

	Grid.prototype.indexOfCellAtPos = function(cellPos)
	{
		return cellPos.y * this.sizeInCells.x + cellPos.x;
	}

	Grid.prototype.openCellPos = function()
	{
		var cellPos = new Coords();

		for (var y = 0; y < this.sizeInCells.y; y++)
		{
			cellPos.y = y;

			var cellValue = null;

			for (var x = 0; x < this.sizeInCells.x; x++)
			{
				cellPos.x = x;
				
				cellValue = this.cellAtPosGet(cellPos);

				if (cellValue == null)
				{
					break;
				}
			}

			if (cellValue == null)
			{
				break;
			}
		}

		return cellPos;
	}

	Grid.prototype.randomize = function()
	{
		var numberOfCells = this.sizeInCells.x * this.sizeInCells.y;

		for (var i = 0; i < numberOfCells; i++)
		{
			this.cells[i] = null;
		}

		for (var i = 0; i < numberOfCells - 1; i++)
		{
			var cellIndex = Math.floor
			(
				Math.random() * numberOfCells
			);

			while (this.cells[cellIndex] != null)
			{
				cellIndex++;
				if (cellIndex >= numberOfCells)
				{
					cellIndex = 0;
				}
			}

			this.cells[cellIndex] = i;
		}

		return this;
	}

	Grid.prototype.slideAtCursorIfPossible = function()
	{
		var openCellPos = this.openCellPos();
		var displacement = openCellPos.clone().subtract
		(
			this.cursorPos
		);
		var distance = displacement.magnitude();
		if (distance == 1)
		{
			var cellValueToSlide = this.cellAtPosGet(this.cursorPos);
			this.cellAtPosSet(this.cursorPos, null);
			this.cellAtPosSet(openCellPos, cellValueToSlide);
		}
	}

	// drawable

	Grid.prototype.drawToDisplay = function(display)
	{
		var cellSizeInPixels = 
			display.sizeInPixels.clone().divide
			(
				this.sizeInCells
			);

		var cellSizeInPixelsHalf = 
			cellSizeInPixels.clone().divideScalar(2);

		var cellPos = new Coords();
		var drawPos = new Coords();
		var cellInde;
		var cellValue;

		for (var y = 0; y < this.sizeInCells.y; y++)
		{
			cellPos.y = y;

			for (var x = 0; x < this.sizeInCells.x; x++)
			{
				cellPos.x = x;

				cellValue = this.cellAtPosGet(cellPos);
				if (cellValue == null)
				{
					cellValue = "";
				}

				drawPos.overwriteWith
				(	
					cellPos
				).multiply
				(
					cellSizeInPixels
				);

				display.drawRectangle
				(
					drawPos, 
					cellSizeInPixels,
					display.colorBack, // fill
					display.colorFore // border
				);

				drawPos.add
				(
					cellSizeInPixelsHalf
				);

				display.drawText
				(
					"" + cellValue,
					drawPos,
					display.colorFore
				);
			}
		}

		drawPos.overwriteWith
		(
			this.cursorPos
		).multiply
		(
			cellSizeInPixels
		);

		display.drawRectangle
		(
			drawPos,
			cellSizeInPixels,
			display.colorFore, // fill
			display.colorBack // border
		);	

		drawPos.add
		(
			cellSizeInPixelsHalf
		);

		cellValue = this.cellAtPosGet(this.cursorPos);
		if (cellValue == null)
		{
			cellValue = "";
		}

		display.drawText
		(
			"" + cellValue,
			drawPos,
			display.colorBack
		);
	}
}

function InputHelper()
{
	// do nothing
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		var keyPressed = event.key;

		var grid = Globals.Instance.grid;

		if (keyPressed == "ArrowDown")
		{
			grid.cursorMove(new Coords(0, 1));
		}
		else if (keyPressed == "ArrowLeft")
		{
			grid.cursorMove(new Coords(-1, 0));
		}
		else if (keyPressed == "ArrowRight")
		{
			grid.cursorMove(new Coords(1, 0));
		}
		else if (keyPressed == "ArrowUp")
		{
			grid.cursorMove(new Coords(0, -1));
		}
		else if (keyPressed == "Enter")
		{
			grid.slideAtCursorIfPossible();
		}

		grid.drawToDisplay(Globals.Instance.display);

	}
}

// run

main();

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

A Multiple Text Document Editor in JavaScript

Below is a multiple text document editor implemented in JavaScript. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit https://thiscouldbebetter.neocities.org/multipletextdocumenteditor.html.


<html>

<style>

.bordered {
	border: 1px solid;
}

.number {
	width: 64px;
}
 
</style>
<body>

<! -- ui -->

<div>
	<div id="divProjectFile" class="bordered">
		<label>Project:</label>
		<div>
			<label>Name:</label>
			<input id="inputProjectName" onchange="inputProjectName_Changed(this);"></input>
		</div>
		<button onclick="buttonProjectSave_Clicked();">Save</button>
		<button onclick="buttonProjectLoad_Clicked();">Load:</button>
		<input id="inputFileProjectToLoad" type="file"></input>
		<button onclick="buttonProjectNew_Clicked();">New</button>
	</div>

	<div id="divDocumentList" class="bordered">
		<label>Documents:</label>
		<div>
			<button onclick="buttonDocumentNew_Clicked();">New</button>		
			<button onclick="buttonDocumentExistingAdd_Clicked();">Add Existing:</button>
			<input id="inputFileDocumentExistingToAdd" type="file"></input>
			<button onclick="buttonDocumentSelectedRemove_Clicked();">Remove Selected</button>
		</div>
		<select id="selectDocumentsInProject" style="width:100%" size="10" onchange="selectDocumentsInProject_Changed(this);"></select>
	</div>

	<div id="divDocumentSelected" class="bordered">
		<div><label>Document Selected:</label></div>
		<div>
			<label>Name:</label>
			<input id="inputDocumentSelectedName" onchange="inputDocumentSelectedName_Changed(this);"></input>
			<button onclick="buttonDocumentSelectedSave_Clicked();">Save</button>
			<label>Cursor:</label>
			<label>Row:</label>
			<input id="inputCursorRow" class="number" type="number" onchange="inputCursorColumnOrRow_Changed(this);"></input>
			<label>Column:</label>
			<input id="inputCursorColumn" class="number" type="number" onchange="inputCursorColumnOrRow_Changed(this);"></input>

		</div>
		<div><label>Contents:</label></div>
		<div><textarea id="textareaDocumentSelectedContents" style="width:100%" rows="20" onchange="textareaDocumentSelectedContents_Changed(this);" onkeydown="textareaDocumentSelectedContents_CursorMoved(this);" onmousedown="textareaDocumentSelectedContents_CursorMoved(this);"></textarea></div>
	</div>

	<div id="divSearch" class="bordered">
		<div>
			<button onclick="buttonSearch_Clicked();">Search for:</button>
			<input id="inputTextToSearchFor"></input>
			<input id="checkboxSearchMatchCase" type="checkbox">Match Case</input>
		</div>
		<div>
			<div><label>Results:</label></div>
			<select id="selectSearchResults" size="8" style="width:100%" onchange="selectSearchResults_Changed(this);"></select>
		</div>
		
	</div>

	</div>

</div>

<!-- ui ends -->

<script type="text/javascript">

// ui events

function buttonDocumentExistingAdd_Clicked()
{
	var project = Globals.Instance.session.project;

	var inputFileDocumentExistingToAdd = document.getElementById
	(
		"inputFileDocumentExistingToAdd"
	);
	var fileToLoad = inputFileDocumentExistingToAdd.files[0];
	if (fileToLoad == null)
	{
		alert("Please choose a file with the accompanying 'Choose File' button first.");
	}
	else
	{
		var documentAdded = FileHelper.loadFileAsText
		(
			fileToLoad,
			buttonDocumentExistingAdd_Clicked_FileLoaded // callback
		);
	}
}

function buttonDocumentExistingAdd_Clicked_FileLoaded(fileLoadedName, fileLoadedContents)
{
	var project = Globals.Instance.session.project;

	var documentToAdd = new Document
	(
		fileLoadedName,
		fileLoadedContents
	);
	project.documentAdd(documentToAdd);

	project.domUpdate();
}

function buttonDocumentNew_Clicked()
{
	var project = Globals.Instance.session.project;

	project.documentNew();

	project.domUpdate();
}

function buttonDocumentSelectedRemove_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		project.documentRemove(documentSelected);
	}

	project.domUpdate();
}

function buttonDocumentSelectedSave_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		FileHelper.saveTextAsFile
		(
			documentSelected.contents, documentSelected.name
		);
	}

	project.domUpdate();
}

function buttonProjectLoad_Clicked()
{
	var project = Globals.Instance.session.project;

	var inputFileProjectToLoad = document.getElementById
	(
		"inputFileProjectToLoad"
	);
	var fileToLoad = inputFileProjectToLoad.files[0];
	if (fileToLoad == null)
	{
		alert("Please choose a file with the accompanying 'Choose File' button first.");
	}
	else
	{
		FileHelper.loadFileAsBytes
		(
			fileToLoad,
			buttonProjectLoad_Clicked_FileLoaded // callback
		);
	}
}

function buttonProjectLoad_Clicked_FileLoaded(fileLoadedName, fileLoadedContentsAsBytes)
{
	var projectAsTar = TarFile.fromBytes(fileLoadedName, fileLoadedContentsAsBytes);
	var documentsAsTarEntries = projectAsTar.entries;
	var documents = [];

	for (var i = 0; i < documentsAsTarEntries.length; i++)
	{
		var documentAsTarEntry = documentsAsTarEntries[i];
		var documentName = documentAsTarEntry.header.fileName;
		var documentContentsAsBytes = documentAsTarEntry.dataAsBytes;
		var documentContentsAsString = ByteHelper.bytesToStringUTF8(documentContentsAsBytes);
		var document = new Document(documentName, documentContentsAsString);
		documents.push(document);
	}

	var projectLoaded = new Project
	(
		fileLoadedName,
		documents
	);

	Globals.Instance.session.project = projectLoaded;

	projectLoaded.domUpdate();
}

function buttonProjectNew_Clicked()
{
	var projectNew = new Project
	(
		"Untitled.tar", []
	);

	Globals.Instance.session.project = projectNew;

	projectNew.domUpdate();

}


function buttonProjectSave_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var projectAsTarFile = project.toTarFile();
	var projectAsBytes = projectAsTarFile.toBytes();
	FileHelper.saveBytesAsFile(projectAsBytes, project.name);
}

function buttonSearch_Clicked()
{
	var project = Globals.Instance.session.project;
	
	var inputTextToSearchFor = document.getElementById
	(
		"inputTextToSearchFor"
	);
	var textToSearchFor = inputTextToSearchFor.value;

	var checkboxSearchMatchCase = document.getElementById
	(
		"checkboxSearchMatchCase"
	);
	var matchCase = checkboxSearchMatchCase.checked;

	project.searchForText(textToSearchFor, matchCase);

	project.domUpdate();
}

function inputDocumentSelectedName_Changed(inputDocumentSelectedName)
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var valueFromDOM = inputDocumentSelectedName.value;
		documentSelected.name = valueFromDOM;
	}

	//project.domUpdate();	
}

function inputCursorColumnOrRow_Changed()
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var inputCursorColumn = document.getElementById("inputCursorColumn");
		var inputCursorRow = document.getElementById("inputCursorRow");

		documentSelected.cursorPos.x = Math.floor(inputCursorColumn.value);
		documentSelected.cursorPos.y = Math.floor(inputCursorRow.value);

		project.domUpdate_Cursor_Place();
	}
}

function inputProjectName_Changed(inputProjectName)
{
	var project = Globals.Instance.session.project;

	var valueFromDOM = inputProjectName.value;
	documentSelected.name = valueFromDOM;

	//project.domUpdate();	
}

function selectDocumentsInProject_Changed(selectDocumentsInProject)
{
	var project = Globals.Instance.session.project;

	var documentIndex = selectDocumentsInProject.selectedIndex;
	project.documentIndexSelected = documentIndex;

	project.domUpdate();
}

function selectSearchResults_Changed(selectSearchResults)
{
	var project = Globals.Instance.session.project;

	var searchResultSelectedIndex = selectSearchResults.selectedIndex;
	var searchResultSelected = project.searchResults[searchResultSelectedIndex];
	if (searchResultSelected != null)
	{
		var documentName = searchResultSelected.documentName;
		var documentToSelect = project.documents[documentName];
		var documentIndex = project.documents.indexOf(documentToSelect);
		project.documentIndexSelected = documentIndex;
		documentToSelect.cursorPos.overwriteWith(searchResultSelected.posInDocument);
		project.domUpdate_Cursor_Place();
	}	


}

function textareaDocumentSelectedContents_Changed(textareaDocumentSelectedContents)
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var valueFromDOM = textareaDocumentSelectedContents.value;
		documentSelected.contents = valueFromDOM;
	}

	//project.domUpdate();
}

function textareaDocumentSelectedContents_CursorMoved(textareaDocumentSelectedContents)
{
	var project = Globals.Instance.session.project;

	var documentSelected = project.documentSelected();
	if (documentSelected != null)
	{
		var text = textareaDocumentSelectedContents.value;
		var cursorOffsetInChars = textareaDocumentSelectedContents.selectionEnd;

		var cursorPosNew = Document.stringAndCharOffsetToCursorPos
		(
			text,
			cursorOffsetInChars
		);

		documentSelected.cursorPos.overwriteWith(cursorPosNew);

		project.domUpdate_Cursor();
	}
}

// main

function main()
{
	var welcomeDocumentContents = "";

	for (var i = 0; i < 32; i++)
	{
		welcomeDocumentContents += "Welcome to the multiple document editor!\n";
	}

	var projectDemo = new Project
	(
		"Welcome.tar",
		[
			new Document
			(
				"Welcome.txt", 
				welcomeDocumentContents
			),
			new Document
			(
				"Welcome2.txt", 
				welcomeDocumentContents
			)

		]
	);
	Globals.Instance.initialize(projectDemo);	
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}

		return this;
	}

	Array.prototype.remove = function(elementToRemove)
	{
		this.splice(this.indexOf(elementToRemove), 1);
	}
}

function StringExtensions()
{
	// extension class
}
{
	String.prototype.padLeft = function(lengthToPadTo, charToPadWith)
	{
		var returnValue = this;

		while (returnValue.length < lengthToPadTo)
		{
			returnValue = charToPadWith + returnValue;
		}

		return returnValue;
	}


	String.prototype.padRight = function(lengthToPadTo, charToPadWith)
	{
		var returnValue = this;

		while (returnValue.length < lengthToPadTo)
		{
			returnValue += charToPadWith;
		}

		return returnValue;
	}
}

// classes

function ByteHelper()
{
	// static class
}
{
	ByteHelper.stringUTF8ToBytes = function(stringToConvert)
	{
		var bytes = [];

		for (var i = 0; i < stringToConvert.length; i++)
		{
			var byte = stringToConvert.charCodeAt(i);
			bytes.push(byte);
		} 

		return bytes;	
	}

	ByteHelper.bytesToStringUTF8 = function(bytesToConvert)
	{
		var returnValue = "";

		for (var i = 0; i < bytesToConvert.length; i++)
		{
			var byte = bytesToConvert[i];
			var byteAsChar = String.fromCharCode(byte);
			returnValue += byteAsChar
		}

		return returnValue;
	}
}

function ByteStream(bytes)
{
	this.bytes = bytes;  

	this.byteIndexCurrent = 0;
}
{
	// constants

	ByteStream.BitsPerByte = 8;
	ByteStream.BitsPerByteTimesTwo = ByteStream.BitsPerByte * 2;
	ByteStream.BitsPerByteTimesThree = ByteStream.BitsPerByte * 3;

	// instance methods

	ByteStream.prototype.hasMoreBytes = function()
	{
		return (this.byteIndexCurrent < this.bytes.length);
	}
	
	ByteStream.prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.readByte();
		}

		return returnValue;
	}

	ByteStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteIndexCurrent];

		this.byteIndexCurrent++;

		return returnValue;
	}

	ByteStream.prototype.readString = function(lengthOfString)
	{
		var returnValue = "";

		for (var i = 0; i < lengthOfString; i++)
		{
			var byte = this.readByte();

			if (byte != 0)
			{
				var byteAsChar = String.fromCharCode(byte);
				returnValue += byteAsChar;
			}
		}

		return returnValue;
	}

	ByteStream.prototype.writeBytes = function(bytesToWrite)
	{
		for (var b = 0; b < bytesToWrite.length; b++)
		{
			this.bytes.push(bytesToWrite[b]);
		}

		this.byteIndexCurrent = this.bytes.length;
	}

	ByteStream.prototype.writeByte = function(byteToWrite)
	{
		this.bytes.push(byteToWrite);

		this.byteIndexCurrent++;
	}

	ByteStream.prototype.writeString = function(stringToWrite, lengthPadded)
	{	
		for (var i = 0; i < stringToWrite.length; i++)
		{
			var charAsByte = stringToWrite.charCodeAt(i);
			this.writeByte(charAsByte);
		}
		
		var numberOfPaddingChars = lengthPadded - stringToWrite.length;
		for (var i = 0; i < numberOfPaddingChars; i++)
		{
			this.writeByte(0);
		}
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

	Coords.prototype.toString = function()
	{
		return this.y + ":" + this.x;
	}
}

function Document(name, contents)
{
	this.name = name;
	this.contents = contents;

	this.cursorPos = new Coords(0, 0);
}
{
	// static methods

	Document.stringAndCharOffsetToCursorPos = function(text, cursorOffsetInChars)
	{
		var newline = "\n";
		var newlinesSoFar = 0;
		var offsetCurrent = null;

		while (true)
		{
			var offsetOfNewline = text.indexOf(newline, offsetCurrent);
			if (offsetOfNewline == -1 || offsetOfNewline >= cursorOffsetInChars)
			{
				break;
			}
			offsetCurrent = offsetOfNewline + 1;
			newlinesSoFar++;
		}

		var returnValue = new Coords
		(
			cursorOffsetInChars - offsetCurrent,
			newlinesSoFar + 1
		);

		return returnValue;
	}

	Document.stringAndCursorPosToCharOffset = function(text, cursorPos)
	{
		var newline = "\n";
		var newlinesSoFar = 0;
		var offsetCurrent = null;
	
		while (offsetCurrent < text.length && newlinesSoFar < cursorPos.y - 1)
		{
			var offsetOfNewline = text.indexOf(newline, offsetCurrent);
			offsetCurrent = offsetOfNewline + 1;
			newlinesSoFar++;
		}

		var returnValue = offsetCurrent + cursorPos.x;

		return returnValue;
	}

}

function FileHelper()
{
	// static class
}
{
    	FileHelper.loadFileAsBytes = function(fileToLoad, callback)
	{   
		var fileReader = new FileReader();
		fileReader.onload = function(fileLoadedEvent)
		{
			var fileLoadedAsBinaryString = 
				fileLoadedEvent.target.result;
			var fileLoadedAsBytes = 
				ByteHelper.stringUTF8ToBytes(fileLoadedAsBinaryString);
			callback(fileToLoad.name, fileLoadedAsBytes);
		}
 
		fileReader.readAsBinaryString(fileToLoad);
	}

	FileHelper.loadFileAsText = function(fileToLoad, callback)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(fileLoadedEvent) 
		{
			var textFromFileLoaded = fileLoadedEvent.target.result;
			callback(fileToLoad.name, textFromFileLoaded);
		};
		fileReader.readAsText(fileToLoad);
	}
 
	FileHelper.saveBytesAsFile = function(bytesToWrite, fileNameToSaveAs)
	{
		var bytesToWriteAsArrayBuffer = new ArrayBuffer(bytesToWrite.length);
		var bytesToWriteAsUIntArray = new Uint8Array(bytesToWriteAsArrayBuffer);
		for (var i = 0; i < bytesToWrite.length; i++) 
		{
			bytesToWriteAsUIntArray[i] = bytesToWrite[i];
		}
 
		var bytesToWriteAsBlob = new Blob
		(
			[ bytesToWriteAsArrayBuffer ], 
			{ type:"application/type" }
		);
 
		var downloadLink = document.createElement("a");
		downloadLink.download = fileNameToSaveAs;
		downloadLink.href = window.URL.createObjectURL(bytesToWriteAsBlob);
		downloadLink.click();
	}

	FileHelper.saveTextAsFile = function(textToSave, fileNameToSaveAs)
	{
		var textToSaveAsBlob = new Blob([textToSave], {type:"text/plain"});
		var textToSaveAsURL = window.URL.createObjectURL(textToSaveAsBlob);

		var downloadLink = document.createElement("a");
		downloadLink.download = fileNameToSaveAs;
		downloadLink.href = textToSaveAsURL;
		downloadLink.click();
	}
}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	Globals.prototype.initialize = function(project)
	{
		this.session = new Session(project);
		this.domUpdate();
	}

	// dom 
	
	Globals.prototype.domUpdate = function()
	{
		this.session.domUpdate();	
	}
}

function Project(name, documents)
{
	this.name = name;
	this.documents = documents.addLookups("name");

	if (this.documents.length > 0)
	{
		this.documentIndexSelected = 0;
	}

	this.searchResults = [];
}
{
	Project.prototype.documentAdd = function(documentToAdd)
	{
		this.documents.push(documentToAdd);
		this.documents[documentToAdd.name] = documentToAdd;
		this.documentIndexSelected = this.documents.length - 1;
	}

	Project.prototype.documentNew = function()
	{
		var documentNew = new Document("Untitled.txt", "");
		this.documentAdd(documentNew);
	}

	Project.prototype.documentRemove = function(documentToRemove)
	{
		this.documents.remove(documentToRemove);
		delete this.documents[documentToRemove.name];
		if (this.documents.length == 0)
		{
			this.documentIndexSelected = null;
		}
	}

	Project.prototype.documentSelected = function()
	{
		return (this.documentIndexSelected == null ? null : this.documents[this.documentIndexSelected]);
	}

	Project.prototype.searchForText = function(textToSearchFor, matchCase)
	{
		this.searchResults.length = 0;

		if (matchCase == false)
		{
			textToSearchFor = textToSearchFor.toLowerCase();
		}

		for (var i = 0; i < this.documents.length; i++)
		{
			var documentToSearch = this.documents[i];
			var documentContents = documentToSearch.contents;

			if (matchCase == false)
			{
				documentContents = documentContents.toLowerCase();
			}

			var indexOfMatchInContents = -1;
			while (true)
			{
				indexOfMatchInContents = documentContents.indexOf
				(
					textToSearchFor,
					indexOfMatchInContents + 1
				);

				if (indexOfMatchInContents >= 0)
				{
					var matchPos = Document.stringAndCharOffsetToCursorPos
					(
						documentContents,
						indexOfMatchInContents
					);

					var newline = "\n";

					var lineWithMatchStart = documentContents.lastIndexOf(newline, indexOfMatchInContents);
					var lineWithMatchEnd = documentContents.indexOf(newline, indexOfMatchInContents);

					if (lineWithMatchStart == -1)
					{
						lineWithMatchStart = 0;
					}

					if (lineWithMatchEnd == -1)
					{
						lineWithMatchEnd = null;
					}

					var lineWithMatch = documentToSearch.contents.substring 
					(
						// Not the same as ".substr"!
						lineWithMatchStart, lineWithMatchEnd
					);

					var result = new SearchResult
					(
						documentToSearch.name, 
						matchPos,
						lineWithMatch
					)
					this.searchResults.push(result);
				}
				else
				{
					break;
				}
			}
		}
	}

	// dom

	Project.prototype.domUpdate = function()
	{
		var inputProjectName = 
			document.getElementById("inputProjectName");

		inputProjectName.value = this.name;

		var selectDocumentsInProject = 
			document.getElementById("selectDocumentsInProject");

		selectDocumentsInProject.options.length = 0;		

		for (var i = 0; i < this.documents.length; i++)
		{
			var _document = this.documents[i];
			var documentAsOption = document.createElement("option");
			documentAsOption.innerHTML = _document.name;
			selectDocumentsInProject.appendChild(documentAsOption);
		}

		var documentSelected = this.documentSelected();

		var inputDocumentSelectedName = 
			document.getElementById("inputDocumentSelectedName");
		var textareaDocumentSelectedContents = 
			document.getElementById("textareaDocumentSelectedContents");

		if (documentSelected == null)
		{
			inputDocumentSelectedName.value = "";
			textareaDocumentSelectedContents.value = "";
		}
		else
		{
			selectDocumentsInProject.selectedIndex = 
				this.documentIndexSelected;
			inputDocumentSelectedName.value = 
				documentSelected.name;
			textareaDocumentSelectedContents.value 
				= documentSelected.contents;
		}

		this.domUpdate_Cursor();

		this.domUpdate_Search();
	}

	Project.prototype.domUpdate_Cursor = function()
	{
		var inputCursorColumn = 
			document.getElementById("inputCursorColumn");
		var inputCursorRow = 
			document.getElementById("inputCursorRow");

		var documentSelected = this.documentSelected();

		if (documentSelected == null)
		{
			inputCursorRow.value = "";
			inputCursorColumn.value = "";
		}
		else
		{
			inputCursorRow.value = documentSelected.cursorPos.y;
			inputCursorColumn.value = documentSelected.cursorPos.x;
		}
	}

	Project.prototype.domUpdate_Cursor_Place = function()
	{	
		var documentSelected = this.documentSelected();

		if (documentSelected != null)
		{
			var cursorOffsetInChars = Document.stringAndCursorPosToCharOffset
			(
				documentSelected.contents,
				documentSelected.cursorPos
			);

			var textareaDocumentSelectedContents = 
				document.getElementById("textareaDocumentSelectedContents");

			textareaDocumentSelectedContents.selectionStart = cursorOffsetInChars; 
			textareaDocumentSelectedContents.selectionEnd = cursorOffsetInChars;
			textareaDocumentSelectedContents.focus();
		}
	}

	Project.prototype.domUpdate_Search = function()
	{
		var selectSearchResults = document.getElementById("selectSearchResults");
		selectSearchResults.innerHTML = "";

		for (var i = 0; i < this.searchResults.length; i++)
		{
			var searchResult = this.searchResults[i];
			var searchResultAsOption = document.createElement("option");
			searchResultAsOption.innerHTML = searchResult.toString();
			selectSearchResults.appendChild(searchResultAsOption);
		}
	}

	// tar

	Project.prototype.toTarFile = function()
	{
		var returnValue = TarFile.new();

		for (var i = 0; i < this.documents.length; i++)
		{
			var _document = this.documents[i];

			var documentContents = _document.contents;
			var documentContentsAsBytes = ByteHelper.stringUTF8ToBytes
			(
				documentContents
			);

			var documentAsTarFileEntry = TarFileEntry.fileNew
			(
				_document.name,
				documentContentsAsBytes	
			);
			returnValue.entries.push(documentAsTarFileEntry);
		}

		return returnValue;
	}
}

function SearchResult(documentName, posInDocument, lineContainingMatch)
{
	this.documentName = documentName;
	this.posInDocument = posInDocument;
	this.lineContainingMatch = lineContainingMatch;
}
{
	SearchResult.prototype.toString = function()
	{
		return this.documentName 
		+ " - " + this.posInDocument.toString() 
		+ " - " + this.lineContainingMatch;
	}
}


function Session(project)
{
	this.project = project;
}
{
	// dom

	Session.prototype.domUpdate = function()
	{
		this.project.domUpdate();
	}
}

function TarFile(fileName, entries)
{
	this.fileName = fileName;
	this.entries = entries;
}
{
	// constants

	TarFile.ChunkSize = 512;

	// static methods

	TarFile.fromBytes = function(fileName, bytes)
	{
		var reader = new ByteStream(bytes);

		var entries = [];

		var chunkSize = TarFile.ChunkSize;

		var numberOfConsecutiveZeroChunks = 0;

		while (reader.hasMoreBytes() == true)
		{
			var chunkAsBytes = reader.readBytes(chunkSize);

			var areAllBytesInChunkZeroes = true;

			for (var b = 0; b < chunkAsBytes.length; b++)
			{
				if (chunkAsBytes[b] != 0)
				{
					areAllBytesInChunkZeroes = false;
					break;
				}
			}

			if (areAllBytesInChunkZeroes == true)
			{
				numberOfConsecutiveZeroChunks++;

				if (numberOfConsecutiveZeroChunks == 2)
				{
					break;
				}
			}
			else
			{
				numberOfConsecutiveZeroChunks = 0;

				var entry = TarFileEntry.fromBytes(chunkAsBytes, reader);

				entries.push(entry);
			}
		}

		var returnValue = new TarFile
		(
			fileName,
			entries
		);

		return returnValue;
	}
	
	TarFile.new = function(fileName)
	{
		return new TarFile
		(
			fileName,
			[] // entries
		);
	}

	// instance methods
	
	TarFile.prototype.downloadAs = function(fileNameToSaveAs)
	{	
		FileHelper.saveBytesAsFile
		(
			this.toBytes(),
			fileNameToSaveAs
		)
	}	
	
	TarFile.prototype.entriesForDirectories = function()
	{
		var returnValues = [];
		
		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			if (entry.header.typeFlag.name == "Directory")
			{
				returnValues.push(entry);
			}
		}
		
		return returnValues;
	}
	
	TarFile.prototype.toBytes = function()
	{
		var fileAsBytes = [];		

		// hack - For easier debugging.
		var entriesAsByteArrays = [];
		
		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			var entryAsBytes = entry.toBytes();
			entriesAsByteArrays.push(entryAsBytes);
		}		
		
		for (var i = 0; i < entriesAsByteArrays.length; i++)
		{
			var entryAsBytes = entriesAsByteArrays[i];
			fileAsBytes = fileAsBytes.concat(entryAsBytes);
		}
		
		var chunkSize = TarFile.ChunkSize;
		
		var numberOfZeroChunksToWrite = 2;
		
		for (var i = 0; i < numberOfZeroChunksToWrite; i++)
		{
			for (var b = 0; b < chunkSize; b++)
			{
				fileAsBytes.push(0);
			}
		}

		return fileAsBytes;
	}
	
	// strings

	TarFile.prototype.toString = function()
	{
		var newline = "\n";

		var returnValue = "[TarFile]" + newline;

		for (var i = 0; i < this.entries.length; i++)
		{
			var entry = this.entries[i];
			var entryAsString = entry.toString();
			returnValue += entryAsString;
		}

		returnValue += "[/TarFile]" + newline;

		return returnValue;
	}
}

function TarFileEntry(header, dataAsBytes)
{
	this.header = header;
	this.dataAsBytes = dataAsBytes;
}
{
	// methods
	
	// static methods
	
	TarFileEntry.directoryNew = function(directoryName)
	{
		var header = new TarFileEntryHeader.directoryNew(directoryName);
		
		var entry = new TarFileEntry(header, []);
		
		return entry;
	}
	
	TarFileEntry.fileNew = function(fileName, fileContentsAsBytes)
	{
		var header = new TarFileEntryHeader.fileNew(fileName, fileContentsAsBytes);
		
		var entry = new TarFileEntry(header, fileContentsAsBytes);
		
		return entry;
	}
	
	TarFileEntry.fromBytes = function(chunkAsBytes, reader)
	{
		var chunkSize = TarFile.ChunkSize;
	
		var header = TarFileEntryHeader.fromBytes
		(
			chunkAsBytes
		);
	
		var sizeOfDataEntryInBytesUnpadded = header.fileSizeInBytes;	

		var numberOfChunksOccupiedByDataEntry = Math.ceil
		(
			sizeOfDataEntryInBytesUnpadded / chunkSize
		)
	
		var sizeOfDataEntryInBytesPadded = 
			numberOfChunksOccupiedByDataEntry
			* chunkSize;
	
		var dataAsBytes = reader.readBytes
		(
			sizeOfDataEntryInBytesPadded
		).slice
		(
			0, sizeOfDataEntryInBytesUnpadded
		);
	
		var entry = new TarFileEntry(header, dataAsBytes);
		
		return entry;
	}
	
	TarFileEntry.manyFromByteArrays = function
	(
		fileNamePrefix, fileNameSuffix, entriesAsByteArrays
	)
	{
		var returnValues = [];
		
		for (var i = 0; i < entriesAsByteArrays.length; i++)
		{
			var entryAsBytes = entriesAsByteArrays[i];
			var entry = TarFileEntry.fileNew
			(		
				fileNamePrefix + i + fileNameSuffix,
				entryAsBytes
			);
			
			returnValues.push(entry);
		}
		
		return returnValues;
	}
	
	// instance methods

	TarFileEntry.prototype.download = function(event)
	{
		FileHelper.saveBytesAsFile
		(
			this.dataAsBytes,
			this.header.fileName
		);
	}
	
	TarFileEntry.prototype.remove = function(event)
	{
		alert("Not yet implemented!"); // todo
	}
	
	TarFileEntry.prototype.toBytes = function()
	{
		var entryAsBytes = [];
	
		var chunkSize = TarFile.ChunkSize;
	
		var headerAsBytes = this.header.toBytes();
		entryAsBytes = entryAsBytes.concat(headerAsBytes);
		
		entryAsBytes = entryAsBytes.concat(this.dataAsBytes);

		var sizeOfDataEntryInBytesUnpadded = this.header.fileSizeInBytes;	

		var numberOfChunksOccupiedByDataEntry = Math.ceil
		(
			sizeOfDataEntryInBytesUnpadded / chunkSize
		)
	
		var sizeOfDataEntryInBytesPadded = 
			numberOfChunksOccupiedByDataEntry
			* chunkSize;
			
		var numberOfBytesOfPadding = 
			sizeOfDataEntryInBytesPadded - sizeOfDataEntryInBytesUnpadded;
	
		for (var i = 0; i < numberOfBytesOfPadding; i++)
		{
			entryAsBytes.push(0);
		}
		
		return entryAsBytes;
	}	
		
	// strings
	
	TarFileEntry.prototype.toString = function()
	{
		var newline = "\n";

		headerAsString = this.header.toString();

		var dataAsHexadecimalString = ByteHelper.bytesToStringHexadecimal
		(
			this.dataAsBytes
		);

		var returnValue = 
			"[TarFileEntry]" + newline
			+ headerAsString
			+ "[Data]"
			+ dataAsHexadecimalString
			+ "[/Data]" + newline
			+ "[/TarFileEntry]"
			+ newline;

		return returnValue
	}
	
}

function TarFileEntryHeader
(
	fileName,
	fileMode,
	userIDOfOwner,
	userIDOfGroup,
	fileSizeInBytes,
	timeModifiedInUnixFormat,
	checksum,
	typeFlag,
	nameOfLinkedFile,
	uStarIndicator,
	uStarVersion,
	userNameOfOwner,
	groupNameOfOwner,
	deviceNumberMajor,
	deviceNumberMinor,
	filenamePrefix
)
{
	this.fileName = fileName;
	this.fileMode = fileMode;
	this.userIDOfOwner = userIDOfOwner;
	this.userIDOfGroup = userIDOfGroup;
	this.fileSizeInBytes = fileSizeInBytes;
	this.timeModifiedInUnixFormat = timeModifiedInUnixFormat;
	this.checksum = checksum;
	this.typeFlag = typeFlag;
	this.nameOfLinkedFile = nameOfLinkedFile;
	this.uStarIndicator = uStarIndicator;
	this.uStarVersion = uStarVersion;
	this.userNameOfOwner = userNameOfOwner;
	this.groupNameOfOwner = groupNameOfOwner;
	this.deviceNumberMajor = deviceNumberMajor;
	this.deviceNumberMinor = deviceNumberMinor;
	this.filenamePrefix = filenamePrefix;
}
{
	TarFileEntryHeader.SizeInBytes = 500;

	// static methods
	
	TarFileEntryHeader.default = function()
	{
		var now = new Date();
		var unixEpoch = new Date(1970, 1, 1);
		var millisecondsSinceUnixEpoch = now - unixEpoch;
		var secondsSinceUnixEpoch = Math.floor
		(
			millisecondsSinceUnixEpoch / 1000
		);
		var secondsSinceUnixEpochAsStringOctal = 
			secondsSinceUnixEpoch.toString(8).padRight(12, " ");
		var timeModifiedInUnixFormat = []; 
		for (var i = 0; i < secondsSinceUnixEpochAsStringOctal.length; i++)
		{
			var digitAsASCIICode = 
				secondsSinceUnixEpochAsStringOctal.charCodeAt(i);
			timeModifiedInUnixFormat.push(digitAsASCIICode);
		}

		var returnValue = new TarFileEntryHeader
		(
			"".padRight(100, "\0"), // fileName
			"100777 \0", // fileMode
			"0 \0".padLeft(8, " "), // userIDOfOwner
			"0 \0".padLeft(8, " "), // userIDOfGroup
			0, // fileSizeInBytes
			timeModifiedInUnixFormat,
			0, // checksum
			TarFileTypeFlag.Instances.Normal,		
			"".padRight(100, "\0"), // nameOfLinkedFile,
			"".padRight(6, "\0"), // uStarIndicator,
			"".padRight(2, "\0"), // uStarVersion,
			"".padRight(32, "\0"), // userNameOfOwner,
			"".padRight(32, "\0"), // groupNameOfOwner,
			"".padRight(8, "\0"), // deviceNumberMajor,
			"".padRight(8, "\0"), // deviceNumberMinor,
			"".padRight(155, "\0") // filenamePrefix	
		);		
		
		return returnValue;
	}
	
	TarFileEntryHeader.directoryNew = function(directoryName)
	{
		var header = TarFileEntryHeader.default();
		header.fileName = directoryName;
		header.typeFlag = TarFileTypeFlag.Instances.Directory;
		header.fileSizeInBytes = 0;
		header.checksumCalculate();
		
		return header;
	}
	
	TarFileEntryHeader.fileNew = function(fileName, fileContentsAsBytes)
	{
		var header = TarFileEntryHeader.default();
		header.fileName = fileName;
		header.typeFlag = TarFileTypeFlag.Instances.Normal;
		header.fileSizeInBytes = fileContentsAsBytes.length;
		header.checksumCalculate();
		
		return header;
	}

	TarFileEntryHeader.fromBytes = function(bytes)
	{
		var reader = new ByteStream(bytes);

		var fileName = reader.readString(100).trim();
		var fileMode = reader.readString(8);
		var userIDOfOwner = reader.readString(8);
		var userIDOfGroup = reader.readString(8);
		var fileSizeInBytesAsStringOctal = reader.readString(12);
		var timeModifiedInUnixFormat = reader.readBytes(12);
		var checksumAsStringOctal = reader.readString(8);
		var typeFlagValue = reader.readString(1);
		var nameOfLinkedFile = reader.readString(100);
		var uStarIndicator = reader.readString(6);
		var uStarVersion = reader.readString(2);
		var userNameOfOwner = reader.readString(32);
		var groupNameOfOwner = reader.readString(32);
		var deviceNumberMajor = reader.readString(8);
		var deviceNumberMinor = reader.readString(8);
		var filenamePrefix = reader.readString(155);
		var reserved = reader.readBytes(12);

		var fileSizeInBytes = parseInt
		(
			fileSizeInBytesAsStringOctal.trim(), 8
		);
		
		var checksum = parseInt
		(
			checksumAsStringOctal, 8
		);		
		
		var typeFlags = TarFileTypeFlag.Instances._All;
		var typeFlagID = "_" + typeFlagValue;
		var typeFlag = typeFlags[typeFlagID];

		var returnValue = new TarFileEntryHeader
		(
			fileName,
			fileMode,
			userIDOfOwner,
			userIDOfGroup,
			fileSizeInBytes,
			timeModifiedInUnixFormat,
			checksum,
			typeFlag,
			nameOfLinkedFile,
			uStarIndicator,
			uStarVersion,
			userNameOfOwner,
			groupNameOfOwner,
			deviceNumberMajor,
			deviceNumberMinor,
			filenamePrefix
		);

		return returnValue;
	}

	// instance methods
	
	TarFileEntryHeader.prototype.checksumCalculate = function()
	{	
		var thisAsBytes = this.toBytes();
	
		// The checksum is the sum of all bytes in the header,
		// except we obviously can't include the checksum itself.
		// So it's assumed that all 8 of checksum's bytes are spaces (0x20=32).
		// So we need to set this manually.
						
		var offsetOfChecksumInBytes = 148;
		var numberOfBytesInChecksum = 8;
		var presumedValueOfEachChecksumByte = " ".charCodeAt(0);
		for (var i = 0; i < numberOfBytesInChecksum; i++)
		{
			var offsetOfByte = offsetOfChecksumInBytes + i;
			thisAsBytes[offsetOfByte] = presumedValueOfEachChecksumByte;
		}
		
		var checksumSoFar = 0;

		for (var i = 0; i < thisAsBytes.length; i++)
		{
			var byteToAdd = thisAsBytes[i];
			checksumSoFar += byteToAdd;
		}		

		this.checksum = checksumSoFar;
		
		return this.checksum;
	}
	
	TarFileEntryHeader.prototype.toBytes = function()
	{
		var headerAsBytes = [];
		var writer = new ByteStream(headerAsBytes);
		
		var fileSizeInBytesAsStringOctal = (this.fileSizeInBytes.toString(8) + " ").padLeft(12, " ")
		var checksumAsStringOctal = (this.checksum.toString(8) + " \0").padLeft(8, " ");

		writer.writeString(this.fileName, 100);
		writer.writeString(this.fileMode, 8);
		writer.writeString(this.userIDOfOwner, 8);
		writer.writeString(this.userIDOfGroup, 8);
		writer.writeString(fileSizeInBytesAsStringOctal, 12);
		writer.writeBytes(this.timeModifiedInUnixFormat);
		writer.writeString(checksumAsStringOctal, 8);
		writer.writeString(this.typeFlag.value, 1);		
		writer.writeString(this.nameOfLinkedFile, 100);
		writer.writeString(this.uStarIndicator, 6);
		writer.writeString(this.uStarVersion, 2);
		writer.writeString(this.userNameOfOwner, 32);
		writer.writeString(this.groupNameOfOwner, 32);
		writer.writeString(this.deviceNumberMajor, 8);
		writer.writeString(this.deviceNumberMinor, 8);
		writer.writeString(this.filenamePrefix, 155);
		writer.writeString("".padRight(12, "\0")); // reserved

		return headerAsBytes;
	}		
		
	// strings

	TarFileEntryHeader.prototype.toString = function()
	{		
		var newline = "\n";
	
		var returnValue = 
			"[TarFileEntryHeader "
			+ "fileName='" + this.fileName + "' "
			+ "typeFlag='" + (this.typeFlag == null ? "err" : this.typeFlag.name) + "' "
			+ "fileSizeInBytes='" + this.fileSizeInBytes + "' "
			+ "]"
			+ newline;

		return returnValue;
	}
}	

function TarFileTypeFlag(value, name)
{
	this.value = value;
	this.id = "_" + this.value;
	this.name = name;
}
{
	TarFileTypeFlag.Instances = new TarFileTypeFlag_Instances();

	function TarFileTypeFlag_Instances()
	{
		this.Normal 		= new TarFileTypeFlag("0", "Normal");
		this.HardLink 		= new TarFileTypeFlag("1", "Hard Link");
		this.SymbolicLink 	= new TarFileTypeFlag("2", "Symbolic Link");
		this.CharacterSpecial 	= new TarFileTypeFlag("3", "Character Special");
		this.BlockSpecial 	= new TarFileTypeFlag("4", "Block Special");
		this.Directory		= new TarFileTypeFlag("5", "Directory");
		this.FIFO		= new TarFileTypeFlag("6", "FIFO");
		this.ContiguousFile 	= new TarFileTypeFlag("7", "Contiguous File");

		// Additional types not implemented:
		// 'g' - global extended header with meta data (POSIX.1-2001)
		// 'x' - extended header with meta data for the next file in the archive (POSIX.1-2001)
		// 'A'–'Z' - Vendor specific extensions (POSIX.1-1988)
		// [other values] - reserved for future standardization

		this._All = 
		[
			this.Normal,
			this.HardLink,
			this.SymbolicLink,
			this.CharacterSpecial,
			this.BlockSpecial,
			this.Directory,
			this.FIFO,
			this.ContiguousFile,
		];

		for (var i = 0; i < this._All.length; i++)
		{
			var item = this._All[i];
			this._All[item.id] = item;
		}
	}
}
	
// run

main();

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , | Leave a comment

A Word-for-Word Inline Translation Utility in JavaScript

The JavaScript program below, when run, prompts the user for a passage to be translated into a foreign language and a dictionary that provides translations, then presents the translated text interleaved with the original text.

The translation is word-for-word, which obviously is not sufficient to translate meaning in most languages. But it does provide a good basis for further practice in learning to read a foreign language.

inlinetranslation


<html>
<body>

<!-- ui -->

	<div>
		<label>Passage to Translate:</label>
		<input type="file" onchange="inputFilePassageToTranslate_Changed(this);"></input>
		<br />
		<textarea id="textareaPassageToTranslate" cols="40" rows="5">This is a test!</textarea>
	</div>

	<div>
		<label>Dictionary to Translate with:</label>
		<input type="file" onchange="inputFileDictionaryToTranslateWith_Changed(this);"></input>
		<br />
		<textarea id="textareaDictionaryToTranslateWith" cols="40" rows="5">
this=esto
is=es
a=un
test=prueba
		</textarea>
	</div>

	<div>
		<button onclick="buttonTranslate_Clicked();">Translate</button>
	</div>

	<div>	
		<label>Interleaved Translation:</label>
		<br />
		<textarea id="textareaTranslationInterleaved" cols="40" rows="10"></textarea>
	</div>

<!-- ui ends -->

<script type="text/javascript">

// ui events

function inputFilePassageToTranslate_Changed(inputFilePassageToTranslate)
{
	var fileToLoad = inputFilePassageToTranslate.files[0];
	var fileReader = new FileReader();
	fileReader.onload = inputFilePassageToTranslate_Changed_FileLoaded;
	fileReader.readAsText(fileToLoad);
}

function inputFilePassageToTranslate_Changed_FileLoaded(event)
{
	var passageToTranslate = event.target.result;
	var textareaPassageToTranslate = document.getElementById
	(
		"textareaPassageToTranslate"
	);
	textareaPassageToTranslate.value = passageToTranslate;
}

function inputFileDictionaryToTranslateWith_Changed(inputFileDictionaryToTranslateWith)
{
	var fileToLoad = inputFileDictionaryToTranslateWith.files[0];
	var fileReader = new FileReader();
	fileReader.onload = inputFileDictionaryToTranslateWith_Changed_FileLoaded;
	fileReader.readAsText(fileToLoad);		
}

function inputFileDictionaryToTranslateWith_Changed_FileLoaded(event)
{
	var dictionaryAsText = event.target.result;
	var textareaDictionaryToTranslateWith = document.getElementById
	(
		"textareaDictionaryToTranslateWith"
	);
	textareaDictionaryToTranslateWith.value = dictionaryAsText;
}

function buttonTranslate_Clicked()
{
	var textareaPassageToTranslate = document.getElementById
	(
		"textareaPassageToTranslate"
	);
	var passageToTranslate = textareaPassageToTranslate.value;

	var textareaDictionaryToTranslateWith = document.getElementById
	(
		"textareaDictionaryToTranslateWith"
	);
	var dictionaryAsText = textareaDictionaryToTranslateWith.value;

	var dictionaryAsLookup = Translator.dictionaryParse(dictionaryAsText);

	var translator = new Translator(dictionaryAsLookup);

	var textareaTranslationInterleaved = document.getElementById
	(
		"textareaTranslationInterleaved"
	);

	var charsPerLine = textareaTranslationInterleaved.cols;

	var textTranslatedAndInterleaved = translator.translateAndInterleave
	(
		passageToTranslate,
		charsPerLine
	);

	textareaTranslationInterleaved.value = textTranslatedAndInterleaved;
}

// extensions

function StringExtensions()
{
	// extension class
}
{
	String.prototype.padRight = function(charToPadWith, lengthToPadTo)
	{
		var returnValue = this;

		while (returnValue.length < lengthToPadTo)
		{
			returnValue += charToPadWith;
		}	

		return returnValue;
	}
}

// classes

function Translator(dictionary)
{
	this.dictionary = dictionary;
}
{
	Translator.dictionaryParse = function(dictionaryAsText)
	{
		var returnValue = {};

		var entriesAsStrings = dictionaryAsText.split("\n");
		for (var i = 0; i < entriesAsStrings.length; i++)
		{
			var entryAsString = entriesAsStrings[i];
			var entryAsKeyAndValue = entryAsString.split("=");
			var key = entryAsKeyAndValue[0];
			var value = entryAsKeyAndValue[1];
			returnValue[key] = value;
		}

		return returnValue;
	}

	// instance methods

	Translator.prototype.translateAndInterleave = function(passageToTranslate, charsPerLine)
	{
		var passageInterleaved = "";

		var wordsToTranslate = passageToTranslate.split(" ");

		var charsInCurrentLine = 0;
		var lineToTranslateCurrent = "";
		var lineTranslatedCurrent = "";
	
		for (var i = 0; i < wordsToTranslate.length; i++)
		{
			var wordToTranslate = wordsToTranslate[i];
			var wordToTranslateAsKey = 
				wordToTranslate.toLowerCase().split("!").join("");

			var wordTranslated = this.dictionary[wordToTranslateAsKey];
			if (wordTranslated == null)
			{
				wordTranslated = "[" + wordToTranslate + "]";
			}

			var wordToTranslateLength = wordToTranslate.length;
			var wordTranslatedLength = wordTranslated.length;

			var wordLengthGreater = Math.max
			(
				wordToTranslateLength,
				wordTranslatedLength
			);
		
			var wordToTranslatePadded = wordToTranslate.padRight
			(
				" ", wordLengthGreater
			);

			var wordTranslatedPadded = wordTranslated.padRight
			(
				" ", wordLengthGreater
			);

			charsInCurrentLine += (1 + wordLengthGreater);
			if (charsInCurrentLine >= charsPerLine)
			{
				passageInterleaved += 
					lineToTranslateCurrent + "\n"
					+ lineTranslatedCurrent + "\n"
					+ "\n";

				lineToTranslateCurrent = wordToTranslatePadded;
				lineTranslatedCurrent = wordTranslatedPadded;
				charsInCurrentLine = wordLengthGreater;	
			}
			else
			{
				lineToTranslateCurrent += wordToTranslatePadded + " ";
				lineTranslatedCurrent += wordTranslatedPadded + " ";
			}
		}

		// hack
		passageInterleaved += 
			lineToTranslateCurrent + "\n"
			+ lineTranslatedCurrent + "\n"
			+ "\n";

		return passageInterleaved;		
	}
}

</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , | 1 Comment

Simulating Hardware in VHDL Using GHDL

Follow the steps below to create a simple “Hello, World” program in VHDL using GHDL.

VHDL, or “VHSIC Hardware Description Language”, is a programming language used to simulate the operation of computer hardware in software.  It provides a standardized method of designing and testing electronic hardware, which reduces the difficulty of the design’s eventual translation into real, physical components.

GHDL is an open-source simulator that can compile and run VHDL code.

1. Download and extract GHDL.  As of this writing, the latest Windows version is available at the URL “https://github.com/tgingold/ghdl/releases/tag/v0.33&#8221;.

2. Locate “GHDL.exe” within the “bin” directory of the newly extracted GHDL archive.  Make a note of the path of GHDL.exe. If desired, this path can be added to the system’s PATH environment variable.

3. In any convenient location, create a new directory named “VHDLTest”.

4. In the newly created VHDLTest directory, create a new text file named “hello.vhdl”, containing the following text. This code is taken from the file “ghdl.htm” within the GHDL archive.


     --  Hello world program.
     use std.textio.all; --  Imports the standard textio package.

     --  Defines a design entity, without any ports.
     entity hello_world is
     end hello_world;

     architecture behaviour of hello_world is
     begin
        process
           variable l : line;
        begin
           write (l, String'("Hello world!"));
           writeline (output, l);
           wait;
        end process;
     end behaviour;

5. Open a command prompt window and run the following commands, substituting the full path of the file ghdl.exe where necessary:


ghdl.exe -a hello.vhdl
ghdl.exe -e hello_world
ghdl.exe -r hello_world

6. Verify that the message “Hello world!” appears in the command prompt window.

Posted in Uncategorized | Tagged , , , , | Leave a comment

Exploring the PNG Image File Format with a PNG Viewer in JavaScript

The JavaScript program below, when run, prompts the user to upload a PNG file and displays that image. To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

The code makes use of the “pako” library, by Andrey Tupitsin and Vitaly Puzrin, to uncompress the pixel data using the DEFLATE algorithm. For more information about pako, visit “https://github.com/nodeca/pako“.

Obviously there are easier and better ways to display a PNG in JavaScript, or for that matter within a web browser. This code is intended as a straightforward illustration of the process of decoding a PNG, with an eye toward eventually implementing a PNG viewer in some other language, though obviously another decompression library will needed in that case.

pngviewer


<html>
<body>

	<div><label>File to Load:</label></div>
	<div><input type="file" onchange="inputFile_Changed(this);"></input></div>
	<div><label>Output:</label></div>
	<div><div id="divOutput"></div></div>

<script type="text/javascript" src="https://rawgit.com/nodeca/pako/master/dist/pako.js"></script>

<script type="text/javascript">

// ui events

function inputFile_Changed(inputFile)
{
	var fileSpecified = inputFile.files[0];
	if (fileSpecified != null)
	{
		FileHelper.loadFileAsBytes
		(
			fileSpecified, inputFile_Changed_Loaded
		);
	}
}

function inputFile_Changed_Loaded(fileAsBytes)
{
	var fileAsPNG = PNG.fromBytes(fileAsBytes);
	var fileAsCanvas = fileAsPNG.toCanvas();
	var divOutput = document.getElementById("divOutput");
	divOutput.appendChild(fileAsCanvas);
}

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.addLookupArrays = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			var arrayForKey = this[key];
			if (arrayForKey == null)
			{
				arrayForKey = [];
				this[key] = arrayForKey;
			}
			arrayForKey.push(element);
		}
		return this;
	}
}

// classes

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteOffset = 0;
}
{
	// constants

	ByteStream.BitsPerByte = 8;

	// methods

	ByteStream.prototype.hasMoreBytes = function()
	{
		var returnValue = (this.byteOffset < this.bytes.length);
		return returnValue;
	}

	ByteStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteOffset];
		this.byteOffset++;
		return returnValue;
	}

	ByteStream.prototype.readBytes = function(numberOfBytes)
	{
		var returnBytes = [];

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.readByte();
			returnBytes.push(byte);
		}

		return returnBytes;
	}

	ByteStream.prototype.readInteger32 = function()
	{
		var returnValue = 0;

		var numberOfBytes = 4;

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.readByte();
			var iReversed = numberOfBytes - i - 1;
			var valueOfByteInPlace = 
				byte << (iReversed * ByteStream.BitsPerByte);
			returnValue += valueOfByteInPlace;
		}

		return returnValue;
	}

	ByteStream.prototype.readString = function(numberOfBytes)
	{
		var returnString = "";

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.readByte();
			var byteAsChar = String.fromCharCode(byte);
			returnString += byteAsChar;
		}

		return returnString;
	}
}

function Color(componentsRGBA)
{
	this.componentsRGBA = componentsRGBA;
}
{
	Color.prototype.clear = function()
	{
		for (var i = 0; i < this.componentsRGBA.length; i++)
		{
			this.componentsRGBA[i] = 0;
		}
	}

	Color.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i < this.componentsRGBA.length; i++)
		{
			this.componentsRGBA[i] = other.componentsRGBA[i];
		}
	}
}

function FileHelper()
{
	// static class
}
{
	FileHelper.binaryStringToBytes = function(binaryStringToConvert)
	{
		var returnBytes = [];

		for (var i = 0; i < binaryStringToConvert.length; i++)
		{
			var byte = binaryStringToConvert.charCodeAt(i);
			returnBytes.push(byte);
		}

		return returnBytes;
	}

	FileHelper.loadFileAsBytes = function(fileToLoad, callback)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var fileAsBinaryString = fileReader.result;
			var fileAsBytes = FileHelper.binaryStringToBytes
			(
				fileAsBinaryString
			);
			callback(fileAsBytes);
		}
		fileReader.readAsBinaryString(fileToLoad);
	}
}

function PNG(signature, chunks)
{
	this.signature = signature;
	this.chunks = chunks;

	this.chunks.addLookupArrays("typeCode");
}
{
	PNG.fromBytes = function(pngAsBytes)
	{
		var byteStream = new ByteStream(pngAsBytes);

		var signature = byteStream.readBytes(8);

		var chunks = [];
		
		while (byteStream.hasMoreBytes() == true)
		{
			var chunkPayloadSizeInBytes = byteStream.readInteger32();
			var chunkTypeCode = byteStream.readString(4);
			var chunkPayloadBytes = byteStream.readBytes(chunkPayloadSizeInBytes);
			var chunkChecksum = byteStream.readInteger32();

			var chunk = new PNG_Chunk
			(
				chunkTypeCode,
				chunkPayloadBytes,
				chunkChecksum
			);

			chunks.push(chunk);
		}

		var returnValue = new PNG(signature, chunks);

		return returnValue;
	}

	// dom

	PNG.prototype.toCanvas = function()
	{
		var headerChunk = this.chunks["IHDR"][0];
		var headerReader = new ByteStream
		(
			headerChunk.payloadBytes
		);

		var imageWidth = headerReader.readInteger32();
		var imageHeight = headerReader.readInteger32();
		
		var bitDepth = headerReader.readByte();

		var colorType = headerReader.readByte(); 

		var bytesPerPixel;
		if (colorType == 0) // grayscale without alpha
		{
			throw "Not yet implemented.";
		}
		else if (colorType == 2) // color without alpha
		{
			bytesPerPixel = 3;
		}
		else if (colorType == 4) // grayscale with alpha
		{
			throw "Not yet implemented.";
		}
		else if (colorType == 6) // color with alpha
		{
			bytesPerPixel = 4;
		}	

		var compressionMethod = headerReader.readByte();

		var filterMethod = headerReader.readByte();

		var interlaceMode = headerReader.readByte();
		
		var dataChunks = this.chunks["IDAT"]; // todo

		var pixelsCompressed = [];

		for (var i = 0; i < dataChunks.length; i++)
		{
			var dataChunk = dataChunks[i];

			pixelsCompressed = pixelsCompressed.concat
			(
				dataChunk.payloadBytes
			);
		}

		var pixelsDecompressed = pako.inflate
		(
			pixelsCompressed
		);

		// An alternative to the pako library is available at
		// https://rawgit.com/imaya/zlib.js/master/bin/zlib.min.js

		// var pixelsDecompressed = new Zlib.Inflate(pixelsCompressed).decompress();

		var canvas = document.createElement("canvas");
		canvas.width = imageWidth;
		canvas.height = imageHeight; 

		var pixelsDefilteredSoFar = [];

		var pixelReader = new ByteStream(pixelsDecompressed);

		var pixelRGBAZeroes = [0, 0, 0, 0];

		for (var y = 0; y < imageHeight; y++)
		{
			var filterTypeCode = pixelReader.readByte();

			for (var x = 0; x < imageWidth; x++)
			{
				var pixelRGBAFiltered = pixelReader.readBytes(bytesPerPixel);
				
				var pixelRGBADefiltered = [];

				for (var c = 0; c < pixelRGBAFiltered.length; c++)
				{
					var pixelComponentFiltered = pixelRGBAFiltered[c];

					var pixelComponentDefiltered;

					if (filterTypeCode == 0) // no filter
					{
						pixelComponentDefiltered = pixelComponentFiltered;
					}
					else if (filterTypeCode == 1) // "sub"
					{
						// Difference of this pixel's component
						// and the corresponding component 
						// of the pixel to the left.

						var pixelLeft;
						if (x == 0)
						{
							pixelLeft = pixelRGBAZeroes;
						}
						else
						{
							var pixelLeftIndex = 
								pixelsDefilteredSoFar.length - 1;
							pixelLeft = pixelsDefilteredSoFar[pixelLeftIndex];
						}
						var pixelLeftComponent = pixelLeft[c];
						pixelComponentDefiltered = 
							(pixelComponentFiltered + pixelLeftComponent) % 256;
					}
					else if (filterTypeCode == 2) // "up"
					{
						// Difference of this pixel's component
						// and the corresponding component 
						// of the pixel above.

						var pixelAboveIndex = 
							pixelsDefilteredSoFar.length - imageWidth;
						var pixelAbove = pixelsDefilteredSoFar[pixelAboveIndex];
						var pixelAboveComponent = pixelAbove[c];
						pixelComponentDefiltered = 
							(pixelComponentFiltered + pixelAboveComponent) % 256;
					}
					else if (filterTypeCode == 3) // "average"
					{
						// Average of left and above.

						var pixelLeft;
						var pixelAbove;

						if (x == 0)
						{
							pixelLeft = pixelRGBAZeroes;
						}
						else
						{
							var pixelLeftIndex = 
								pixelsDefilteredSoFar.length - 1;

							pixelLeft = pixelsDefilteredSoFar[pixelLeftIndex];
						}

						var pixelAboveIndex = 
							pixelsDefilteredSoFar.length - imageWidth;
						pixelAbove = pixelsDefilteredSoFar[pixelAboveIndex];

						var pixelLeftComponent = pixelLeft[c];
						var pixelAboveComponent = pixelAbove[c];


						pixelComponentDefiltered = 
							pixelComponentFiltered 
							+ Math.floor
							(
								(pixelLeftComponent + pixelAboveComponent)
								/2
							);

						pixelComponentDefiltered = pixelComponentDefiltered % 256;
					}
					else if (filterTypeCode == 4) // "Paeth"
					{
						// Uses left, above, and above left. 
						var pixelLeft;
						var pixelAbove;
						var pixelAboveLeft;

						if (x == 0)
						{
							pixelLeft = pixelRGBAZeroes;
							pixelAboveLeft = pixelRGBAZeroes;
						}
						else
						{
							var pixelLeftIndex = 
								pixelsDefilteredSoFar.length - 1;

							pixelLeft = pixelsDefilteredSoFar[pixelLeftIndex];

							if (y == 0)
							{
								pixelAboveLeft = pixelRGBAZeroes;
							}
							else
							{

								var pixelAboveLeftIndex = 
									pixelsDefilteredSoFar.length - 1 - imageWidth;

								pixelAboveLeft = pixelsDefilteredSoFar[pixelAboveLeftIndex];
							}
						}

						if (y == 0)
						{
							pixelAbove = pixelRGBAZeroes;
						}
						else
						{
							var pixelAboveIndex = 
								pixelsDefilteredSoFar.length - imageWidth;
							pixelAbove = pixelsDefilteredSoFar[pixelAboveIndex];
						}

						var pixelLeftComponent = pixelLeft[c];
						var pixelAboveLeftComponent = pixelAboveLeft[c];
						var pixelAboveComponent = pixelAbove[c];

						var paethValue = this.paethPredictor
						(
							pixelLeftComponent, 
							pixelAboveComponent,
							pixelAboveLeftComponent 
						);

						pixelComponentDefiltered = 
							(pixelComponentFiltered + paethValue) % 256;
					}
					else
					{
						throw "Unknown filter type."
					}

					pixelRGBADefiltered[c] = pixelComponentDefiltered;
				}

				pixelsDefilteredSoFar.push(pixelRGBADefiltered);
			}
		}

		var graphics = canvas.getContext("2d");

		for (var y = 0; y < imageHeight; y++)
		{
			for (var x = 0; x < imageWidth; x++)
			{
				var pixelIndex = y * imageWidth + x;
				var pixelRGBA = pixelsDefilteredSoFar[pixelIndex];

				var pixelColorAsString = 
					"(" + pixelRGBA[0] + "," 
					+ pixelRGBA[1] + "," 
					+ pixelRGBA[2]; 

				if (pixelRGBA.length == 3)
				{
					pixelColorAsString = 
						"rgb" + pixelColorAsString;
				}
				else
				{
					pixelColorAsString = 
						"rgba" 
						+ pixelColorAsString 
						+ "," + (pixelRGBA[3] / 255);
				}

				pixelColorAsString += ")";

				graphics.fillStyle = pixelColorAsString;
				graphics.fillRect(x, y, 1, 1);
			}
		}
	
		return canvas;
	}

	// helper methods

	PNG.prototype.paethPredictor = function(left, above, aboveLeft)
	{
		// Adapted from pseudocode found at the URL
		// https://www.w3.org/TR/PNG-Filters.html

		var estimate = left + above - aboveLeft;

		var differenceFromLeft = Math.abs(estimate - left);
		var differenceFromAbove = Math.abs(estimate - above);
		var differenceFromAboveLeft = Math.abs(estimate - aboveLeft);

		var returnValue;

		if 
		(
			differenceFromLeft <= differenceFromAbove
			&& differenceFromLeft <= differenceFromAboveLeft
		)
		{ 
			returnValue = left;
		}
		else if (differenceFromAbove <= differenceFromAboveLeft)
		{
			returnValue = above;
		}
		else
		{ 
			returnValue = aboveLeft;
		}

		return returnValue;
   	}
}

function PNG_Chunk(typeCode, payloadBytes, checksum)
{
	this.typeCode = typeCode;
	this.payloadBytes = payloadBytes;
	this.checksum = checksum;
}

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , , , | Leave a comment

Decompressing Data with the DEFLATE Algorithm in JavaScript

The code below, when run, prompts the user to upload a compressed file in GZIP format, which will then be uncompressed, and the uncompressed data will be displayed both as hexadecimal digits and as UTF8 text.

This implementation of DEFLATE is basically a port of a Java implementation created by Nyuki Minase, an edited Java version of which I presented in a previous post. I tried to leave the code in this port as nearly identical as possible, but I had to make a few questionable changes here and there. The most distressing of these is where I commented out the writing of data to the output byte stream in Decompressor.decodeHuffmanBlock(), because it seemed to be appending nonsense to the end of my test data. But no doubt it was there for a reason. It should go without saying that any bugs in this implementation should be assumed to be my own.

I tested the program by creating a simple .txt file and compressing it to a .gz with 7-Zip.

Note that this implementation only does decompression, not compression.

decompressingwithdeflate


<html>
<body>

<!-- ui -->

	<div>
		<div><label>Compressed Bytes as Hexadecimal:</label></div>
		<div><input type="file" onchange="inputCompressedDataLoadFromFile_Changed(this);"></input></div>
		<div><textarea id="textareaCompressed" cols="32" rows="8"></textarea></div>
	</div>

	<div>
		<button onclick="buttonDecompress_Clicked();">Decompress</button>
	</div>

	<div>
		<div><label>Uncompressed Bytes as Hexadecimal:</label></div>
		<div><textarea id="textareaUncompressed" cols="32" rows="8"></textarea></div>

		<div><label>Uncompressed Bytes as Text (UTF8):</label></div>
		<div><textarea id="textareaUncompressedAsUTF8" cols="32" rows="8"></textarea></div>
	</div>

<!-- ui ends -->

<script type="text/javascript">

// extensions

function ArrayExtensions()
{
	// extension class
}
{
	Array.prototype.removeAt = function(index)
	{
		this.splice(index, 1);
		return this;
	}
}

function StringExtensions()
{
	// extension class
}
{
	String.prototype.padLeft = function(charToPadWith, lengthToPadTo)
	{
		var returnValue = this;
		while (returnValue.length < lengthToPadTo)
		{
			returnValue = charToPadWith + returnValue;
		}
		return returnValue;
	}
}

// ui events

function buttonDecompress_Clicked()
{
	var textareaCompressed = document.getElementById("textareaCompressed");	
	var bytesCompressedAsHexadecimal = textareaCompressed.value;
	var bytesCompressed = [];
	for (var i = 0; i < bytesCompressedAsHexadecimal.length; i += 2)
	{
		var byteAsHexadecimal = bytesCompressedAsHexadecimal.substr(i, 2);
		var byte = parseInt(byteAsHexadecimal, 16);
		bytesCompressed.push(byte);
	}

	var inflator = new Inflator();
	var bytesDecompressed = inflator.decompressBytes(bytesCompressed);

	var bytesDecompressedAsHexadecimal = "";
	var bytesDecompressedAsUTF8 = "";
	for (var i = 0; i < bytesDecompressed.length; i++)
	{
		var byte = bytesDecompressed[i];

		var byteAsHexadecimal = byte.toString(16).padLeft("0", 2);
		bytesDecompressedAsHexadecimal += byteAsHexadecimal;

		var byteAsUTF8 = String.fromCharCode(byte);
		bytesDecompressedAsUTF8 += byteAsUTF8;
	}

	var textareaUncompressed = document.getElementById("textareaUncompressed");
	textareaUncompressed.value = bytesDecompressedAsHexadecimal;

	var textareaUncompressedAsUTF8 = document.getElementById("textareaUncompressedAsUTF8");
	textareaUncompressedAsUTF8.value = bytesDecompressedAsUTF8;

}

function inputCompressedDataLoadFromFile_Changed(input)
{
	var file = input.files[0];
	var fileReader = new FileReader();
	fileReader.onload = function(event)
	{
		var fileContentsAsBinaryString = event.target.result;
		var bytesCompressed = fileContentsAsBinaryString;
		var bytesCompressedAsHexadecimal = "";
		for (var i = 0; i < bytesCompressed.length; i++)
		{
			var byte = bytesCompressed.charCodeAt(i);
			var byteAsHexadecimal = byte.toString(16).padLeft("0", 2);
			bytesCompressedAsHexadecimal += byteAsHexadecimal;
		}
		var textareaCompressed = document.getElementById("textareaCompressed");
		textareaCompressed.value = bytesCompressedAsHexadecimal;
	}
	fileReader.readAsBinaryString(file);
}

// DEFLATE implementation

// Most of the code below is a port 
// of a Java implementation of DEFLATE written by Nyuki Minase.

/*(MIT License)

Copyright © 2012 Nayuki Minase
Copyright © 2017 This Could Be Better

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.

*/

// main

function Inflator()
{
	// do nothing
}
{
	Inflator.prototype.decompressBytes = function(fileContentsAsBytes)
	{	
		var input = new BitStream(fileContentsAsBytes);

		let magicNumberForGZIP = input.readInteger16LE();
		if (magicNumberForGZIP != 35615)
			throw "Invalid GZIP magic number";

		var compressionMethodCode = input.readByte();
		if (compressionMethodCode != 8)
			throw "Unsupported compression method: " + (compressionMethodCode & 0xFF);

		var flags = input.readByte();

		// Reserved flags
		if ((flags & 0xE0) != 0)
			throw "Reserved flags are set";

		// Modification time
		var mtime = input.readInteger32LE();
		if (mtime != 0)
		{
			//console.log("Last modified: " + new DateTime(1970, 1, 1).add(mtime * 1000000L));
		}
		else
		{
			console.log("Last modified: N/A");
		}

		var extraFlags = input.readByte();

		// Extra flags
		switch (extraFlags) 
		{
			case 2:   console.log("Extra flags: Maximum compression");  break;
			case 4:   console.log("Extra flags: Fastest compression");  break;
			default:  console.log("Extra flags: Unknown");  break;
		}

		// Operating system
		var osCode = input.readByte();
		var os;
		switch (osCode & 0xFF) 
		{
			case   0:  os = "FAT";             break;
			case   1:  os = "Amiga";           break;
			case   2:  os = "VMS";             break;
			case   3:  os = "Unix";            break;
			case   4:  os = "VM/CMS";          break;
			case   5:  os = "Atari TOS";       break;
			case   6:  os = "HPFS";            break;
			case   7:  os = "Macintosh";       break;
			case   8:  os = "Z-System";        break;
			case   9:  os = "CP/M";            break;
			case  10:  os = "TOPS-20";         break;
			case  11:  os = "NTFS";            break;
			case  12:  os = "QDOS";            break;
			case  13:  os = "Acorn RISCOS";    break;
			case 255:  os = "Unknown";         break;
			default :  os = "Really unknown";  break;
		}
		console.log("Operating system: " + os);

		// Text flag
		if ((flags & 0x01) != 0)
		{
			console.log("Flag: Text");
		}

		// Extra flag
		if ((flags & 0x04) != 0) 
		{
			console.log("Flag: Extra");
			var len = input.readInteger16LE();
			input.readBytes(len);  // Skip extra data
		}

		// File name flag
		if ((flags & 0x08) != 0) 
		{
			var sb = "";
			while (true) 
			{
				var temp = input.readByte();
				if (input.hasMoreBits() == false)
					throw "EOFException";
				else if (temp == 0)  // Null-terminated string
					break;
				else
					sb += String.fromCharCode(temp);
			}
			console.log("File name: " + sb);
		}

		// Header CRC flag
		if ((flags & 0x02) != 0) {
			
			var crc = input.readInteger16LE(2);
			console.log("Header CRC-16: %04X%n", crc);
		}

		// Comment flag
		if ((flags & 0x10) != 0) {
			var sb = "";
			while (true) {
				var temp = input.readByte();
				if (input.hasMoreBits() == false)
					throw "EOFException";
				else if (temp == 0)  // Null-terminated string
					break;
				else
					sb += String.fromCharCode(temp);
			}
			console.log("Comment: " + sb);
		}

		// Decompress
		var bytesDecompressed = Decompressor.decompress
		(
			input
		);

		return bytesDecompressed;
	}
}

// classes

function BitStream(bytes)
{
	this.bytes = bytes;
	this.byteIndex = 0;
	this.bitOffsetWithinByte = 0;
}
{
	BitStream.prototype.alignWithByteBoundary = function()
	{
		if (this.bitOffsetWithinByte != 0)
		{
			this.bitOffsetWithinByte = 0;
			this.byteIndex++;
		}
	}

	BitStream.prototype.hasMoreBits = function()
	{
		return (this.byteIndex < this.bytes.length);
	}

	BitStream.prototype.readBit = function()
	{
		var byteCurrent = this.bytes[this.byteIndex];
		var returnValue = (byteCurrent >> (8 - this.bitOffsetWithinByte - 1)) & 0x1;
		this.bitOffsetWithinByte++;
		if (this.bitOffsetWithinByte >= 8)
		{
			this.bitOffsetWithinByte = 0;
			this.byteIndex++;
		}
		return returnValue;
	}

	BitStream.prototype.readBits = function(numberOfBits)
	{
		var returnValues = [];	

		for (var i = 0; i < numberOfBits; i++)
		{
			var bit = this.readBit();
			returnValues.push(bit);
		}

		return returnValues;
	}

	BitStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteIndex];
		this.byteIndex++;
		return returnValue;
	}

	BitStream.prototype.readBytes = function(numberOfBytes)
	{
		var returnValues = [];

		for (var i = 0; i < numberOfBytes; i++)
		{
			var byte = this.bytes[this.byteIndex];
			returnValues.push(byte);
			this.byteIndex++;
		}

		return returnValues;
	}


	BitStream.prototype.readIntegerOfBitWidthLE = function(numberOfBits)
	{
		var returnValue = 0;

		var bits = this.readBits(numberOfBits);
		for (var i = 0; i < bits.length; i++)
		{
			var bit = bits[i];
			var bitValueInPlace = bit << i;
			returnValue = returnValue | bitValueInPlace;
		}

		return returnValue;
	}

	BitStream.prototype.readInteger16LE = function()
	{
		// 16-bit, little-endian

		var bytes = this.readBytes(2);
		var returnValue = bytes[0] | (bytes[1] << 8);
		return returnValue;
	}

	BitStream.prototype.readInteger32LE = function()
	{
		// 32-bit, little-endian

		var bytes = this.readBytes(4);
		var returnValue = 
			bytes[0] 
			| (bytes[1] << 8) 
			| (bytes[2] << 16) 
			| (bytes[3] << 24);
		return returnValue;
	}

	BitStream.prototype.writeByte = function(byte)
	{
		this.bytes.push(byte);
		this.byteIndex = this.bytes.length;
	}

	BitStream.prototype.writeBytes = function(bytesToWrite)
	{
		for (var i = 0; i < bytesToWrite.length; i++)
		{
			var byte = bytesToWrite[i];
			this.bytes.push(byte);
		}
		this.byteIndex = this.bytes.length;
	}
}

function CanonicalCode(codeLengths)
{

	if (codeLengths == null)
	{
		throw "Argument is null";
	}

	this.codeLengths = codeLengths.slice(0);
	for (var i = 0; i < codeLengths.length; i++) 
	{
		var x = codeLengths[i];
		if (x < 0)
		{
			throw "Illegal code length";
		}
	}
}
{
	// static classes 

	CanonicalCode.constructor2 = function(tree, symbolLimit) 
	{
		var codeLengths = new Array(symbolLimit);
		var returnValue = new CanonicalCode(codeLengths);
		returnValue.buildCodeLengths(tree.root, 0);
	}

	CanonicalCode.prototype.buildCodeLengths = function(node, depth) 
	{
		if (node.constructor.name == "InternalNode") 
		{
			var internalNode = node;
			this.buildCodeLengths(internalNode.leftChild , depth + 1);
			this.buildCodeLengths(internalNode.rightChild, depth + 1);
		} 
		else if (node.constructor.name == "Leaf") 
		{
			var symbol = node.symbol;
			if (codeLengths[symbol] != 0)
			{
				throw "Symbol has more than one code"; 
			}
			if (symbol >= codeLengths.length)
			{
				throw "Symbol exceeds symbol limit";
			}
			this.codeLengths[symbol] = depth;
		} 
		else 
		{
			throw "Illegal node type";
		}
	}

	CanonicalCode.prototype.getSymbolLimit = function() 
	{
		return this.codeLengths.length;
	}

	CanonicalCode.prototype.getCodeLength = function(symbol) 
	{
		if (symbol < 0 || symbol >= this.codeLengths.length)
		{
			throw "Symbol out of range";
		}
		return this.codeLengths[symbol];
	}

	CanonicalCode.prototype.toCodeTree = function() 
	{
		var nodes = [];
		for (var i = this.max(this.codeLengths); i >= 1; i--) 
		{  
			// Descend through positive code lengths
			var newNodes = [];

			// Add leaves for symbols with code length i
			for (var j = 0; j < this.codeLengths.length; j++) {
				if (this.codeLengths[j] == i)
					newNodes.push(new Leaf(j));
			}

			// Merge nodes from the previous deeper layer
			for (var j = 0; j < nodes.length; j += 2)
			{
				newNodes.push(new InternalNode(nodes[j], nodes[j + 1]));
			}

			nodes = newNodes;
			if (nodes.length % 2 != 0)
			{
				throw "This canonical code does not represent a Huffman code tree";
			}
		}

		if (nodes.length != 2)
		{
			throw "This canonical code does not represent a Huffman code tree";
		}
		return new CodeTree(new InternalNode(nodes[0], nodes[1]), this.codeLengths.length);
	}

	CanonicalCode.prototype.max = function(array) 
	{
		var result = array[0];
		for (var i = 0; i < array.length; i++)
		{
			var x = array[i];
			result = Math.max(x, result);
		}
		return result;
	}	
}

function CircularDictionary(size)
{
	this.data = new Array(size);
	this.index = 0;

	if (IntegerMath.isPowerOf2(size))
	{
		this.mask = size - 1;
	}
	else
	{
		this.mask = 0;
	}
}
{
	CircularDictionary.prototype.append = function(b) 
	{
		this.data[this.index] = b;
		if (this.mask != 0)
			this.index = (this.index + 1) & this.mask;
		else
			this.index = (this.index + 1) % this.data.length;
	}

	CircularDictionary.prototype.copy = function(dist, len, out)
	{
		if (len < 0 || dist < 1 || dist > this.data.length)
		{
			throw "IllegalArgumentException";
		}

		if (this.mask != 0) 
		{
			var readIndex = (this.index - dist + this.data.length) & this.mask;
			for (var i = 0; i < len; i++) 
			{
				out.write(this.data[readIndex]);
				this.data[this.index] = this.data[readIndex];
				readIndex = (readIndex + 1) & this.mask;
				this.index = (this.index + 1) & this.mask;
			}
		} 
		else 
		{
			var readIndex = (this.index - dist + this.data.length) % this.data.length;
			for (var i = 0; i < len; i++) 
			{
				out.write(this.data[readIndex]);
				this.data[this.index] = this.data[readIndex];
				readIndex = (readIndex + 1) % this.data.length;
				this.index = (this.index + 1) % this.data.length;
			}
		}
	}	
}

function CodeTree(root, symbolLimit)
{
	// public final InternalNode root;  // Not null

	// Stores the code for each symbol, or null if the symbol has no code.
	// For example, if symbol 5 has code 10011, then codes.get(5) is the list [1, 0, 0, 1, 1].
	// private List<List<Integer>> codes;

	// Every symbol in the tree 'root' must be strictly less than 'symbolLimit'.

	if (root == null)
	{
		throw "Argument is null";
	}
	this.root = root;

	this.codes = [];  // Initially all null
	for (var i = 0; i < symbolLimit; i++)
	{
		this.codes.push(null);
	}

	this.buildCodeList(root, []);  // Fills 'codes' with appropriate data
}
{
	CodeTree.prototype.buildCodeList = function(node, prefix) 
	{
		var nodeConstructorName = node.constructor.name;
		if (nodeConstructorName == "InternalNode") 
		{
			var internalNode = node;

			prefix.push(0);
			this.buildCodeList(internalNode.leftChild , prefix);
			prefix.removeAt(prefix.length - 1);

			prefix.push(1);
			this.buildCodeList(internalNode.rightChild, prefix);
			prefix.removeAt(prefix.length - 1);	
		} 
		else if (nodeConstructorName == "Leaf") 
		{
			var leaf = node;
			if (leaf.symbol >= this.codes.length)
			{
				throw "Symbol exceeds symbol limit";
			}
			if (this.codes[leaf.symbol] != null)
			{
				throw "Symbol has more than one code";
			}
			this.codes[leaf.symbol] = new Array(prefix); // ?
		} 
		else 
		{
			throw "Illegal node type";
		}
	}

	CodeTree.prototype.getCode = function(symbol) 
	{
		if (symbol < 0)
		{
			throw new "Illegal symbol";
		}
		else if (this.codes[symbol] == null)
		{
			throw "No code for given symbol";
		}
		else
		{
			return this.codes[symbol];
		}
	}

	// Returns a string showing all the codes in this tree. The format is subject to change. Useful for debugging.
	CodeTree.prototype.toString = function() 
	{
		var sb = "";
		sb = this.toString2("", root, sb);
		return sb;
	}

	CodeTree.prototype.toString2 = function(prefix, node, sb) 
	{
		var nodeConstructorName = node.constructor.name;
		if (nodeConstructorName == "InternalNode") 
		{
			var internalNode = node;
			sb = this.toString2(prefix + "0", internalNode.leftChild , sb);
			sb = this.toString2(prefix + "1", internalNode.rightChild, sb);
		} 
		else if (nodeConstructorName == "Leaf") 
		{
			sb += String.format("Code %s: Symbol %d%n", prefix, node.symbol);
		} 
		else 
		{
			throw "Illegal node type";
		}

		return sb;
	}	
}

function InternalNode(leftChild, rightChild)
{
	if (leftChild == null || rightChild == null)
	{
		throw "Argument is null";
	}
	this.leftChild = leftChild;
	this.rightChild = rightChild;
}

function Leaf(symbol)
{	
	if (symbol < 0)
		throw "Illegal symbol value";
	this.symbol = symbol;
}

function Decompressor(input)
{
	// These were formerly static variables.

	var llcodelens = new Array(288);
	llcodelens.fill(8, 0, 144);
	llcodelens.fill(9, 144, 256);
	llcodelens.fill(7, 256, 280);
	llcodelens.fill(8, 280, 288);
	this.llcodelens = llcodelens;

	this.fixedLiteralLengthCode = new CanonicalCode(llcodelens).toCodeTree();
	this.distcodelens = new Array(32);

	this.distcodelens.fill(5);
	this.fixedDistanceCode = new CanonicalCode(this.distcodelens).toCodeTree();

	// The original constructor picks up here.

	this.input = input;
	this.output = new BitStream([]);
	this.dictionary = new CircularDictionary(32 * 1024);

	// Process the stream of blocks
	while (true) 
	{
		// Block header
		var isFinal = (input.readBit() == 1);  // bfinal
		var type = input.readIntegerOfBitWidthLE(2); // btype

		// Decompress by type
		if (type == 0)
		{
			this.decompressUncompressedBlock();
		}
		else if (type == 1 || type == 2) 
		{
			var litLenCode, distCode;
			if (type == 1) 
			{
				litLenCode = this.fixedLiteralLengthCode;
				distCode = this.fixedDistanceCode;
			} 
			else 
			{
				var temp = this.decodeHuffmanCodes(input);
				litLenCode = temp[0];
				distCode = temp[1];
			}

			this.decompressHuffmanBlock(litLenCode, distCode);	
		} 
		else if (type == 3)
		{
			throw "Invalid block type";
		}
		else
		{
			throw "AssertionError";
		}

		if (isFinal)
		{
			break;
		}
	}
}
{	
	// static methods

	/* Public method */
	Decompressor.decompress = function(input)
	{
		var decompressor = new Decompressor(input);
		return decompressor.output.bytes;
	}

	// For handling static Huffman codes (btype = 1)
	Decompressor.fixedLiteralLengthCode = null; // todo
	Decompressor.fixedDistanceCode = null; // todo

	// For handling dynamic Huffman codes (btype = 2)
	Decompressor.prototype.decodeHuffmanCodes = function(input)
	{
		var numLitLenCodes = input.readIntegerOfBitWidthLE(5) + 257;  // hlit  + 257
		var numDistCodes = input.readIntegerOfBitWidthLE(5) + 1;      // hdist +   1

		var numCodeLenCodes = input.readIntegerOfBitWidthLE(4) + 4;   // hclen +   4
		var codeLenCodeLen = new Array(19);
		codeLenCodeLen[16] = input.readIntegerOfBitWidthLE(3);
		codeLenCodeLen[17] = input.readIntegerOfBitWidthLE(3);
		codeLenCodeLen[18] = input.readIntegerOfBitWidthLE(3);
		codeLenCodeLen[ 0] = input.readIntegerOfBitWidthLE(3);
		for (var i = 0; i < numCodeLenCodes - 4; i++) 
		{
			if (i % 2 == 0)
				codeLenCodeLen[8 + i / 2] = input.readIntegerOfBitWidthLE(3);
			else
				codeLenCodeLen[7 - i / 2] = input.readIntegerOfBitWidthLE(3);
		}
		var codeLenCode = new CanonicalCode(codeLenCodeLen).toCodeTree();

		var codeLens = new Array(numLitLenCodes + numDistCodes);
		var runVal = -1;
		var runLen = 0;
		for (var i = 0; i < codeLens.length; i++) 
		{
			if (runLen > 0) 
			{
				codeLens[i] = runVal;
				runLen--;	
			} 
			else 
			{
				var sym = this.decodeSymbol(codeLenCode);
				if (sym < 16) 
				{
					codeLens[i] = sym;
					runVal = sym;
				} 
				else 
				{
					if (sym == 16) 
					{
						if (runVal == -1)
						{
							throw "No code length value to copy";
						}
						runLen = input.readIntegerOfBitWidthLE(2) + 3;
					} 
					else if (sym == 17) 
					{
						runVal = 0;
						runLen = input.readIntegerOfBitWidthLE(3) + 3;
					} 
					else if (sym == 18) 
					{
						runVal = 0;
						runLen = input.readIntegerOfBitWidthLE(7) + 11;
					} 
					else
					{
						throw "AssertionError";
					}

					i--;
				}
			}
		}
		if (runLen > 0)
		{
			throw "Run exceeds number of codes";
		}

		// Create code trees
		var litLenCodeLen = codeLens.slice(0, numLitLenCodes); // ?
		var litLenCode = new CanonicalCode(litLenCodeLen).toCodeTree();

		var distCodeLen = codeLens.slice(numLitLenCodes, codeLens.length); // ?
		var distCode;
		if (distCodeLen.length == 1 && distCodeLen[0] == 0)
		{
			distCode = null;  // Empty distance code; the block shall be all literal symbols
		}
		else
		{
			distCode = new CanonicalCode(distCodeLen).toCodeTree();
		}

		return [litLenCode, distCode];
	}

	/* Block decompression methods */

	Decompressor.prototype.decompressUncompressedBlock = function()
	{
		// Discard bits to align to byte boundary
		this.input.alignWithByteBoundary();

		// Read length
		var len  = this.input.readInteger16LE();
		var nlen = this.input.readInteger16LE();
		var nlenCalculated = len ^ 0xFFFF; 
		if (nlenCalculated != nlen) 
		{
			throw "Invalid length in uncompressed block";
		}

		// Copy bytes
		for (var i = 0; i < len; i++) 
		{
			var temp = this.input.readByte();
			if (this.input.hasMoreBits() == false)
			{
				throw "EOFException";
			}
			this.output.writeByte(temp);
			this.dictionary.append(temp);
		}
	}

	Decompressor.prototype.decompressHuffmanBlock = function(litLenCode, distCode)
	{
		if (litLenCode == null)
		{
			throw "NullPointerException";
		}

		while (true) 
		{
			var sym = this.decodeSymbol(litLenCode);
			if (sym == 256)  // End of block
			{
				break;
			}

			if (sym < 256) 
			{  
				// Literal byte

				// Leaving the next line in
				// causes strange junk to appear 
				// at the end of the uncompressed data.
				// Not sure what's going on.
				//this.output.writeByte(sym);

				this.dictionary.append(sym);
			} 
			else 
			{  // Length and distance for copying
				var len = this.decodeRunLength(sym);
				if (distCode == null)
				{
					throw "Length symbol encountered with empty distance code";
				}
				var distSym = this.decodeSymbol(distCode);
				var dist = this.decodeDistance(distSym);
				this.dictionary.copy(dist, len, output);
			}
		}
	}

	/* Symbol decoding methods */
	Decompressor.prototype.decodeSymbol = function(code)
	{
		var currentNode = code.root;
		while (true) 
		{
			var temp = this.input.readBit();
			var nextNode;
			if (temp == 0)
			{			
				nextNode = currentNode.leftChild;
			}
			else if (temp == 1)
			{
				nextNode = currentNode.rightChild;
			}
			else
			{
				throw "AssertionError";
			}

			var nextNodeConstructorName = nextNode.constructor.name;
			if (nextNodeConstructorName == "Leaf")
			{
				return nextNode.symbol;
			}
			else if (nextNodeConstructorName == "InternalNode")
			{
				currentNode = nextNode;
			}
			else
			{
				throw "AssertionError";
			}
		}
	}

	Decompressor.prototype.decodeRunLength = function(sym)
	{
		if (sym < 257 || sym > 285)
		{
			throw "Invalid run length symbol: " + sym;
		}
		else if (sym <= 264)
		{
			return sym - 254;
		}
		else if (sym <= 284) 
		{
			var i = (sym - 261) / 4;  // Number of extra bits to read
			return (((sym - 265) % 4 + 4) << i) + 3 + input.readIntegerOfBitWidthLE(i);
		} 
		else  // sym == 285
		{
			return 258;
		}
	}

	Decompressor.prototype.decodeDistance = function(sym)
	{
		if (sym <= 3)
		{
			return sym + 1;
		}
		else if (sym <= 29) 
		{
			var i = sym / 2 - 1;  // Number of extra bits to read
			return ((sym % 2 + 2) << i) + 1 + input.readIntegerOfBitWidthLE(i);
		} 
		else
		{
			throw "Invalid distance symbol: " + sym;
		}
	}
}

function IntegerMath()
{
	// static class
}
{
	IntegerMath.isPowerOf2 = function(valueToCheck)
	{
		return ((Math.log(valueToCheck) / Math.log(2)) % 0) == 0;
	}
}

</script>
</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment