A Rudimentary Desktop Publishing Application in JavaScript

The code below implements a simple desktop publishing application in JavaScript. To see it 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/desktoppublisher.html.


<html>
<body>

<!-- ui begins -->

<div>
<label>Document as JSON:</label><br />
<textarea id="textareaDocumentAsJSON" cols="80" rows="32" wrap="off" >
{
	"name":"Invictus",
	"pageSizeInPixels":{"x":300,"y":300},
	"fonts": [ { "name":"sans-serif", "heightInPixels":10 } ],
	"pageDefns":
	[
		{
			"name": "PageDefn0",
			"zoneDefns":
			[
				{ "name":"0", "pos":{"x":10,"y":10}, "size":{"x":150,"y":80}, "margin":{"x":20,"y":20}, "pageOffsetNext":0, "zoneNameNext":"1" },
				{ "name":"1", "pos":{"x":70,"y":110}, "size":{"x":150,"y":80}, "margin":{"x":20,"y":20}, "pageOffsetNext":0,"zoneNameNext":"2" },
				{ "name":"2", "pos":{"x":130,"y":210}, "size":{"x":150,"y":80}, "margin":{"x":20,"y":20}, "pageOffsetNext":1,"zoneNameNext":"0"  }
			]
		}
	],
	"contentBlocks": 
	[
		{ 
			"name": "Content0", 
			"typeName": "text", 
			"data": "Out of the night which covers me, black as the pit from pole to pole, I thank whatever gods may be for my unconquerable soul.\n\nIn the fell clutch of circumstance, I have not winced nor cried aloud.  Under the bludgeoning of chance, my head is bloody, but unbowed.\n\nBeyond this place of wrath and tears looms but the Horror of the shade, and yet the menace of the years finds, and shall find me, unafraid.\n\nIt matters not how strait the gate, how charged with punishments the scroll, I am the master of my fate: I am the captain of my soul.\n\n-William Ernest Henley"
		} 
	],
	"pages": 
	[
		{ "defnName": "PageDefn0" },
		{ "defnName": "PageDefn0" },
		{ "defnName": "PageDefn0" }
	],
	"contentAssignments": 
	[
		{ "contentBlockName":"Content0", "pageIndex":0, "zoneName":"0" }
	]
}
</textarea>
</div>

<div>
	<button onclick="buttonDisplay_Clicked();">Display</button>
	<button onclick="buttonExport_Clicked();">Export</button>
</div>


<!-- ui ends -->

<script type="text/javascript">

// ui events

function buttonDisplay_Clicked()
{
	var documentToDisplay = documentParseAndLayOut();
	documentToDisplay.draw();
}

function buttonExport_Clicked()
{
	var documentToExport = documentParseAndLayOut();
	var documentAsTarFile = documentToExport.toTarFile();
	var documentAsBytes = documentAsTarFile.toBytes();
	FileHelper.saveBytesAsFile(documentAsBytes, documentToExport.name + ".tar");
}

function documentParseAndLayOut()
{
	var textareaDocumentAsJSON = document.getElementById
	(
		"textareaDocumentAsJSON"
	);
	var documentAsStringJSON = textareaDocumentAsJSON.value;
	
	var documentAsDeserializedObject = JSON.parse(documentAsStringJSON);

	var documentParsed = Document.fromDeserializedObject
	(
		documentAsDeserializedObject
	);

	documentParsed.initialize();
	documentParsed.update();

	return documentParsed;
}


// 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;
	}
}

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 ContentAssignment(contentBlockName, pageIndex, zoneDefnName)
{
	this.contentBlockName = contentBlockName;
	this.pageIndex = pageIndex;
	this.zoneDefnName = zoneDefnName;
}
{
	ContentAssignment.fromDeserializedObject = function(contentAssignmentAsObject)
	{
		return new ContentAssignment
		(
			contentAssignmentAsObject.contentBlockName,
			contentAssignmentAsObject.pageIndex,
			contentAssignmentAsObject.zoneName
		);
	}
}

function ContentBlock(name, typeName, data)
{
	this.name = name;
	this.typeName = typeName;
	this.data = data;
}
{
	ContentBlock.fromDeserializedObject = function(contentBlockAsObject)
	{
		return new ContentBlock
		(
			contentBlockAsObject.name,
			contentBlockAsObject.typeName,
			contentBlockAsObject.data
		);
	}
}

function ContentType(name)
{
	this.name = name;
}

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

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

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

	// serializable

	Coords.fromDeserializedObject = function(coordsAsObject)
	{
		return new Coords(coordsAsObject.x, coordsAsObject.y);
	}
}

function Display(sizeInPixels, renderToScreen)
{
	this.sizeInPixels = sizeInPixels;
	this.renderToScreen = (renderToScreen == null ? true : renderToScreen);

	this.drawPos = new Coords();
}
{
	Display.prototype.clear = function()
	{
		this.drawRectangle
		(
			new Coords(0, 0), this.sizeInPixels, "White", "Gray"
		);
	}

	Display.prototype.fontSet = function(font)
	{
		this.font = font;
		this.graphics.font = font.toString();
	}

	Display.prototype.initialize = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.sizeInPixels.x;
		this.canvas.height = this.sizeInPixels.y;
		this.graphics = this.canvas.getContext("2d");

		if (this.renderToScreen == true)
		{
			document.body.appendChild(this.canvas);
		}
	}

	Display.prototype.toImageBytes = function()
	{
		var imageAsPNGDataURL = this.canvas.toDataURL("image/png");

		var imageAsByteString = atob(imageAsPNGDataURL.split(',')[1]);
		var imageAsBytes = [];

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

		return imageAsBytes;
	}

	Display.prototype.widthOfText = function(textToMeasure)
	{
		return this.graphics.measureText(textToMeasure).width;
	}

	// primitives

	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)
	{
		this.graphics.fillStyle = "Gray";
		this.graphics.fillText
		(
			text,
			pos.x, pos.y
		);

		
	}
}

function Document(name, pageSizeInPixels, fonts, pageDefns, contentBlocks, pages, contentAssignments)
{
	this.name = name;
	this.pageSizeInPixels = pageSizeInPixels;
	this.fonts = fonts.addLookups("name");
	this.pageDefns = pageDefns.addLookups("name");
	this.contentBlocks = contentBlocks.addLookups("name");
	this.pages = pages;
	this.contentAssignments = contentAssignments;
}
{
	Document.prototype.initialize = function()
	{
		var pages = this.pages;

		for (var i = 0; i < pages.length; i++)
		{
			var page = pages[i];
			page.initialize(this);
		}		
	}

	Document.prototype.update = function()
	{
		var pages = this.pages;

		for (var i = 0; i < pages.length; i++)
		{
			var page = pages[i];
			page.update(this);
		}		
	}

	// drawable

	Document.prototype.draw = function()
	{
		var pages = this.pages;

		for (var i = 0; i < pages.length; i++)
		{
			var page = pages[i];
			page.draw(this);
		}		
	}
	
	// serializable

	Document.fromDeserializedObject = function(documentAsObject)
	{
		var name = documentAsObject.name;

		var pageSizeInPixels = Coords.fromDeserializedObject
		(
			documentAsObject.pageSizeInPixels
		);

		var fonts = [];
		var fontsAsObjects = documentAsObject.fonts;
		for (var i = 0; i < fontsAsObjects.length; i++)
		{
			var fontAsObject = fontsAsObjects[i];
			var font = new Font(fontAsObject.name, fontAsObject.heightInPixels);
			fonts.push(font);
		}

		var pageDefns = [];
		var pageDefnsAsObjects = documentAsObject.pageDefns;
		for (var i = 0; i < pageDefnsAsObjects.length; i++)
		{
			var pageDefnAsObject = pageDefnsAsObjects[i];
			var pageDefn = PageDefn.fromDeserializedObject(pageDefnAsObject);
			pageDefns.push(pageDefn);
		}

		var contentBlocks = [];
		var contentBlocksAsObjects = documentAsObject.contentBlocks;
		for (var i = 0; i < contentBlocksAsObjects.length; i++)
		{
			var contentBlockAsObject = contentBlocksAsObjects[i];
			var contentBlock = ContentBlock.fromDeserializedObject
			(
				contentBlockAsObject
			);
			contentBlocks.push(contentBlock);
		}

		var pages = [];
		var pagesAsObjects = documentAsObject.pages;
		for (var i = 0; i < pagesAsObjects.length; i++)
		{
			var pageAsObject = pagesAsObjects[i];
			var page = Page.fromDeserializedObject(pageAsObject);
			pages.push(page);
		}

		var contentAssignments = [];
		var contentAssignmentsAsObjects = documentAsObject.contentAssignments;
		for (var i = 0; i < contentAssignmentsAsObjects.length; i++)
		{
			var contentAssignmentAsObject = contentAssignmentsAsObjects[i];
			var contentAssignment = ContentAssignment.fromDeserializedObject(contentAssignmentAsObject);
			contentAssignments.push(contentAssignment);
		}

		var returnValue = new Document
		(
			name,
			pageSizeInPixels,
			fonts,
			pageDefns,
			contentBlocks,
			pages,
			contentAssignments
		);

		return returnValue;
	}

	// tar

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

		for (var i = 0; i < this.pages.length; i++)
		{
			var page = this.pages[i];
			page.draw(this, false);
			var pageAsImageBytes = page.display.toImageBytes();

			var pageAsTarFileEntry = TarFileEntry.fileNew
			(
				"Page" + i + ".png",
				pageAsImageBytes
			);

			returnValue.entries.push(pageAsTarFileEntry);
		}

		return returnValue;
	}

}

function Font(name, heightInPixels)
{
	this.name = name;
	this.heightInPixels = heightInPixels;
}
{
	Font.prototype.toString = function()
	{
		return "" + this.sizeInPixels + "px " + this.name;
	}
}

function Page(defnName, zones)
{
	this.defnName = defnName;
	this.zones = zones;
}
{
	Page.prototype.defn = function(document)
	{
		return document.pageDefns[this.defnName];
	}

	Page.prototype.initialize = function(document)
	{
		this.zonesBuild(document);
	}

	Page.prototype.update = function(document)
	{		
		var defn = this.defn(document);
		var zoneDefns = defn.zoneDefns;

		var contentAssignments = document.contentAssignments;

		var pageIndex = document.pages.indexOf(this);

		for (var i = 0; i < contentAssignments.length; i++)
		{
			var contentAssignment = contentAssignments[i];
			if (pageIndex == contentAssignment.pageIndex)
			{
				var contentBlockName = contentAssignment.contentBlockName;
				var contentBlock = document.contentBlocks[contentBlockName];
				var zoneDefnName = contentAssignment.zoneDefnName;
				var zoneDefn = zoneDefns[zoneDefnName];			
				var zoneIndex = zoneDefns.indexOf(zoneDefn);
				var zone = this.zones[zoneIndex];
				zone.contentBlockName = contentBlockName;
			}
		}

		for (var i = 0; i < this.zones.length; i++)
		{
			var zone = this.zones[i];
			zone.update(document, this);
		}
	}

	Page.prototype.zonesBuild = function(document)
	{
		var defn = this.defn(document);

		if (this.zones == null)
		{
			var zoneDefns = defn.zoneDefns;

			var zones = [];

			for (var i = 0; i < zoneDefns.length; i++)
			{
				var zoneDefn = zoneDefns[i];
				var zone = new Zone(zoneDefn.name);
				zones.push(zone);
			}

			this.zones = zones;
		}
	}

	// drawable

	Page.prototype.draw = function(document, renderToScreen)
	{
		var displaySizeInPixels = document.pageSizeInPixels;
		this.display = new Display(displaySizeInPixels, renderToScreen);
		this.display.initialize();

		this.display.clear();

		var zones = this.zones;

		for (var i = 0; i < zones.length; i++)
		{
			var zone = zones[i];
			zone.draw(document, this);
		}		
	}

	// serializable

	Page.fromDeserializedObject = function(pageAsObject)
	{
		var zones = null;

		var zonesAsObjects = pageAsObject.zones;
		if (zonesAsObjects != null)
		{
			zones = [];

			for (var i = 0; i < zonesAsObjects.length; i++)
			{
				var zoneAsObject = zonesAsObjects[i];
				var zone = Zone.fromDeserializedObject(zoneAsObject);
				zones.push(zone);
			}
		}

		var returnValue = new Page
		(
			pageAsObject.defnName,
			zones
		);

		return returnValue;
	}

}

function PageDefn(name, zoneDefns)
{
	this.name = name;
	this.zoneDefns = zoneDefns.addLookups("name");

	for (var z = 0; z < this.zoneDefns.length; z++)
	{
		var zoneDefn = this.zoneDefns[z];
		var zoneDefnNameNext = zoneDefn.zoneDefnNameNext;
		if (zoneDefnNameNext != null)
		{
			zoneDefnNext = this.zoneDefns[zoneDefnNameNext];
			zoneDefnNext.zoneDefnNamePrev = zoneDefn.name;
		}		
	}
}
{
	// serializable

	PageDefn.fromDeserializedObject = function(pageDefnAsObject)
	{
		var zoneDefns = [];

		var zoneDefnsAsObjects = pageDefnAsObject.zoneDefns;
		for (var i = 0; i < zoneDefnsAsObjects.length; i++)
		{
			var zoneDefnAsObject = zoneDefnsAsObjects[i];
			var zoneDefn = ZoneDefn.fromDeserializedObject(zoneDefnAsObject);
			zoneDefns.push(zoneDefn);
		}

		var returnValue = new PageDefn(pageDefnAsObject.name, zoneDefns);

		return returnValue;
	}
}

function Zone(defnName)
{
	this.defnName = defnName;
}
{
	Zone.prototype.content = function(document, page)
	{
		if (this._content == null)
		{
			var contentBlock = document.contentBlocks[this.contentBlockName];
			if (contentBlock != null)
			{
				this._content = contentBlock.data;
			}
		}
		
		return this._content;
	}

	Zone.prototype.defn = function(document, page)
	{
		var pageDefn = page.defn(document);
		var returnValue = pageDefn.zoneDefns[this.defnName];
		return returnValue;
	}

	Zone.prototype.update = function(document, page)
	{
		var zoneDefn = this.defn(document, page);

		var content = this.content(document);

		if (content == null)
		{
			return;
		}

		var contentAsLines = [];

		// hack
		var display = new Display(new Coords(0, 0), false);
		display.initialize();

		var fontName = zoneDefn.fontName;
		if (fontName == null)
		{
			fontName = document.fonts[0].name;
		}
		var font = document.fonts[fontName];
		var fontSizeY = font.heightInPixels;
		var charOffset = new Coords(0, 0);

		var lineCurrent = "";
		var wordCurrent = "";

		for (var i = 0; i < content.length; i++)
		{
			var contentChar = content[i];

			wordCurrent += contentChar;

			var widthOfContentChar = display.widthOfText
			(
				contentChar
			);
			charOffset.x += widthOfContentChar;

			if (contentChar == " ")
			{
				lineCurrent += wordCurrent;
				wordCurrent = "";
			}
			else if (contentChar == "\n")
			{
				lineCurrent += wordCurrent + "\n";
				wordCurrent = "";
				charOffset.x = zoneDefn.sizeMinusMargin.x;
			}

			if (charOffset.x >= zoneDefn.sizeMinusMargin.x)
			{
				charOffset.y += fontSizeY;

				if (charOffset.y >= zoneDefn.sizeMinusMargin.y)
				{
					var pageIndex = document.pages.indexOf(page);
					var pageIndexNext = pageIndex + zoneDefn.pageOffsetNext;
					var pageNext = document.pages[pageIndexNext];
					if (pageNext == null)
					{
						break;
					}
					page = pageNext;
					var zoneNextName = zoneDefn.zoneNameNext;
					var zoneNext = pageNext.zones[zoneNextName];
					if (zoneNext != null)
					{
						zoneNext._content = 
							wordCurrent 
							+ content.substr(i + 1);
						wordCurrent = "";
						zoneNext.update(document, page);
						break;
					}
				}

				contentAsLines.push(lineCurrent);
				lineCurrent = "" + wordCurrent;

				charOffset.x = display.widthOfText(wordCurrent);

				wordCurrent = "";
			}
		}

		lineCurrent += wordCurrent;
		contentAsLines.push(lineCurrent);

		this.contentAsLines = contentAsLines;
	}

	// drawable

	Zone.prototype.draw = function(document, page)
	{
		var zone = this;
		var zoneDefn = zone.defn(document, page);

		var display = page.display;
		var drawPos = display.drawPos;

		var zonePos = zoneDefn.pos;
		var zoneSize = zoneDefn.size;
		var zoneMargin = zoneDefn.margin;
		var zoneSizeMinusMargin = zoneDefn.sizeMinusMargin;

		display.drawRectangle(zonePos, zoneSize, "White", "LightGray");

		var contentAsLines = zone.contentAsLines;

		if (contentAsLines != null)
		{	
			var fontName = zoneDefn.fontName;
			if (fontName == null)
			{
				fontName = document.fonts[0].name;
			}
			var font = document.fonts[fontName];
			var fontSizeY = font.heightInPixels;
			display.fontSet(font);

			for (var i = 0; i < contentAsLines.length; i++)
			{
				var contentLine = contentAsLines[i];

				var widthOfWhitespaceBetweenCharacters;

				if (contentLine.indexOf("\n") >= 0)
				{
					widthOfWhitespaceBetweenCharacters = 0;
				}
				else
				{
					contentLine = contentLine.trim();

					var widthOfLineBeforeJustification = display.widthOfText
					(
						contentLine
					);

					var widthOfWhitespaceBetweenCharacters = 
						(
							zoneSizeMinusMargin.x 
							- widthOfLineBeforeJustification
						)
						/ (contentLine.length - 1); 
				}

				contentLine = contentLine.trim();

				var charOffsetX = 0;

				for (var j = 0; j < contentLine.length; j++)
				{
					var contentChar = contentLine[j];			

					drawPos.overwriteWithXY
					(
						zonePos.x + zoneMargin.x + charOffsetX,
						zonePos.y + zoneMargin.y + fontSizeY * (i + 1)
					);

					display.drawText(contentChar, drawPos);

					var widthOfChar = display.widthOfText
					(
						contentChar
					);

					charOffsetX += 
						widthOfChar
						+ widthOfWhitespaceBetweenCharacters;
				}
			}
		}
	}

	// serialzable

	Zone.fromDeserializedObject = function(zoneAsObject)
	{
		var returnValue = new Zone
		(
			zoneAsObject.defnName
		);

		return returnValue;
	}
}

function ZoneDefn(name, pos, size, margin, pageOffsetNext, zoneNameNext, fontName)
{
	this.name = name;
	this.pos = pos;
	this.size = size;
	this.margin = margin;
	this.zoneNameNext = zoneNameNext;
	this.pageOffsetNext = pageOffsetNext;
	this.fontName = fontName;

	this.zoneNamePrev = null;

	this.sizeMinusMargin = this.size.clone().subtract
	(
		this.margin
	).subtract
	(
		this.margin
	);
}
{
	// serialzable

	ZoneDefn.fromDeserializedObject = function(zoneDefnAsObject)
	{
		var returnValue = new ZoneDefn
		(
			zoneDefnAsObject.name, 
			Coords.fromDeserializedObject(zoneDefnAsObject.pos), 
			Coords.fromDeserializedObject(zoneDefnAsObject.size), 
			Coords.fromDeserializedObject(zoneDefnAsObject.margin), 
			zoneDefnAsObject.pageOffsetNext, 
			zoneDefnAsObject.zoneNameNext, 
			zoneDefnAsObject.fontName
		);

		return returnValue;
	}
}

// libraries

// export

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 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 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;
		}
	}
}

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

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s