Catagolue browser extension

For general discussion about Conway's Game of Life.
User avatar
rowett
Moderator
Posts: 3815
Joined: January 31st, 2013, 2:34 am
Location: UK
Contact:

Re: Catagolue browser extension

Post by rowett » July 12th, 2016, 5:53 pm

Apple Bottom wrote:
rowett wrote:I don't know if it's a direction you're interested in going in but it's possible to put LifeViewer into the extension...
That's neat! Any patches? ;)
It's fairly easy to do and just requires changes to catagoluereloaded.js:
  1. copy lv-plugin.js content into the top of catagoluereloaded.js after the line beginning // On second thought
  2. add a call to startAllViewers(); to the MAIN() function after the line containing objectToRLE(params);

    Code: Select all

    // MAIN function.
    function MAIN() {
    
       // read page parameters
       var params = readParams();
    
       if(params != null) {
    
          // do our work.
          addNavLinks    (params);
          handleSampleSoups(params);
          objectToRLE    (params);
          startAllViewers();
    
       } else {
    
          // this shouldn't happen on pages where this script actually runs.
          console.log("Could not read page parameters.");
    
       }
    }
  3. some additions and modifications to objectToRLE()

    Code: Select all

    // add a textarea with the object in RLE format.
    function objectToRLE(params) {
    
       var prefix = params["prefix"];
       var object = params["object"];
       var rule   = params["rule"  ];
    
       // regex to test prefix
       var prefixRegex = /^x[pqs]/;
    
       // only run for known objects.
       if(object == null)
          return;
    
       // only run for spaceships (xq), oscillators (xp) and still lifes (xs).
       if(!prefixRegex.test(prefix))
          return;
    
       // convert object to pattern, and pattern to RLE.
       var pattern = apgcodeToPattern(object, rule);
       var RLE     = patternToRLE    (pattern);
    
       // find the "Comments" H2
       var commentsH2 = findCommentsH2();
    
       // create a new heading for the RLE code.
       var RLEHeading = document.createElement("h3");
       RLEHeading.textContent = "RLE";
    
       // create "select all" link for RLE code.
       var RLESelectAll     = document.createElement("p");
       var RLESelectAllLink = document.createElement("a");
    
       RLESelectAll.style.marginTop    = 0;
       RLESelectAll.style.marginBottom = "0.5em";
       RLESelectAll.style.fontFamily   = "monospace";
    
       RLESelectAllLink.href        = "#";
       RLESelectAllLink.textContent = "Select All";
       RLESelectAllLink.setAttribute("onclick", 'document.getElementById("RLETextArea").select(); return false');
    
       RLESelectAll.appendChild(RLESelectAllLink);
    
       // create the enclosing div
       var viewerDiv = document.createElement("div");
       viewerDiv.className = "viewer";
       
       // create the canvas
       var viewerCanvas = document.createElement("canvas");
       viewerCanvas.width = "480";
       viewerCanvas.height = "560";
    
       // create a textarea for the RLE code.
       var RLETextArea = document.createElement("textarea");
       RLETextArea.id          = "RLETextArea";
       RLETextArea.style.width = "100%";
       RLETextArea.rows        = "10";
       RLETextArea.readOnly    = true;
       RLETextArea.textContent = RLE;
    
       // add the textarea containing the rle and the canvas to the viewer div
       viewerDiv.appendChild(RLETextArea);
       viewerDiv.appendChild(viewerCanvas);
    
       // insert the new nodes.
       commentsH2.parentNode.insertBefore(RLEHeading,   commentsH2);
       commentsH2.parentNode.insertBefore(RLESelectAll, commentsH2);
       commentsH2.parentNode.insertBefore(viewerDiv,  commentsH2);
    
       // create the meta tag for LifeViewer
       var meta = document.createElement("meta");
       meta.name = "LifeViewer";
       meta.content = "viewer textarea 60 hide";
       document.getElementsByTagName("head")[0].appendChild(meta);
    }
    
Could probably do similar for the soup popup too...

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » July 12th, 2016, 6:18 pm

rowett wrote:It's fairly easy to do and just requires changes to catagoluereloaded.js:
  1. copy lv-plugin.js content into the top of catagoluereloaded.js after the line beginning // On second thought
  2. add a call to startAllViewers(); to the MAIN() function after the line containing objectToRLE(params);
Could probably do similar for the soup popup too...
OK, cool.

Say, since you know Javascript -- do you know if there's a (sane) way to pull in an external file? The script already has three embedded scripts by now that get injected into object pages; they're currently included as strings, but I'd much prefer to read them from files packaged with the extension instead.

Another question: catagoluereloaded.js is basically a user script that manipulates the page once and then quits, rather than running in the context of the (live) page itself; that's why e.g. the sample soup overlay script has to be injected into the page as a separate script. So wouldn't the LifeViewer have to be injected the same way as well?

And third: do you have a non-minified version of the LifeViewer script? Opera's guidelines for addons don't look favorably on minified/obfuscated code, and truth to be told if I'm going to include this code I'd like to glance it over and have some idea of what it's actually doing as well. :)

Thanks!
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
rowett
Moderator
Posts: 3815
Joined: January 31st, 2013, 2:34 am
Location: UK
Contact:

Re: Catagolue browser extension

Post by rowett » July 13th, 2016, 6:56 am

Apple Bottom wrote:do you know if there's a (sane) way to pull in an external file? The script already has three embedded scripts by now that get injected into object pages; they're currently included as strings, but I'd much prefer to read them from files packaged with the extension instead.
I've never written an extension. However on reading some docs on Opera Dev the correct way to include multiple js files is to list them in the manifest.json (see the first "js" line):

Code: Select all

{
   "content_scripts": [ {
      "include_globs": [ "*://catagolue.appspot.com/object/*" ],
      "js": [ "lv-plugin.js", "catagoluereloaded.js" ],
      "matches": [ "*://catagolue.appspot.com/*" ]
   }, {
      "include_globs": [ "*://catagolue.appspot.com/census/*" ],
      "js": [ "symmetryinjector.js" ],
      "matches": [ "*://catagolue.appspot.com/*" ]
   } ],
   "description": "Useful enhancements for Adam P. Goucher's Catagolue",
   "developer": {
      "name": "Apple Bottom",
      "url": "https://twitter.com/_AppleBottom_"
   },
   "icons": {
      "128": "catagolue2.0_128.png",
      "16": "catagolue2.0_16.png",
      "48": "catagolue2.0_48.png"
   },
   "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyerPtI7otzb1B+McYNZJK5erkayw9ePYSzFhtfZ7HAdt0ADcdDAHqJsZySSKDBtbcI+cmPnUjJa+Z2OGbapW5dxbHrrFcF2orN5ZdO1vukVT/94uaJFKSG+JbifC7au2Gi/UMT6PgCF8FZsyQOGTpMsJJBATcrC+Mvy64wCttnws49rkbB0lIGe7OwBZOG2OfbzMbHzNO2KU8j+2kUzRLia/quSbWZlgQiJW7tSZCmyre4E/CzaYznfpGrDwMmEatXXCakTOyI3HvPzEUpXOK6xLU6aVOTNKJFnu8N0gSUAt2fkNCNM92XNu+nY8s3aWEsezBiAx8vjIG7Js08dbFQIDAQAB",
   "manifest_version": 2,
   "name": "Catagolue Reloaded",
   "update_url": "https://extension-updates.opera.com/api/omaha/update/",
   "version": "3.5"
}
So step 1 of my instructions above should be changed. No need to cut and paste the content of lv-plugin.js into catagoluereloaded.js, simply add lv-plugin.js to the manifest.
Apple Bottom wrote:catagoluereloaded.js is basically a user script that manipulates the page once and then quits, rather than running in the context of the (live) page itself; that's why e.g. the sample soup overlay script has to be injected into the page as a separate script. So wouldn't the LifeViewer have to be injected the same way as well?
It appears to work well based on the method above. But again, I've never written an extension before so I'm not the best person to ask.
Apple Bottom wrote:do you have a non-minified version of the LifeViewer script? Opera's guidelines for addons don't look favorably on minified/obfuscated code, and truth to be told if I'm going to include this code I'd like to glance it over and have some idea of what it's actually doing as well. :)
I'm not currently planning on releasing the source for LifeViewer. I started the project in 2012 as a vehicle to learn Javascript/HTML5 and forgot to stop. The commented source is about 25,000 lines of code. Minified and pretty-printed by the browser developer tools you get about 12,500 lines which are at least readable.
If minified code is an issue for Opera then feel free to omit LifeViewer. I was just interested by your extension because I'd never seen one. Nice job, by the way.

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Catagolue browser extension

Post by Scorbie » July 13th, 2016, 12:23 pm

rowett wrote: I started the project in 2012 as a vehicle to learn Javascript/HTML5 and forgot to stop.
Didn't know the projext was that long! Maybe I should try the same to learn myself JS/HTML5.

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » July 13th, 2016, 3:20 pm

rowett wrote:I'm not currently planning on releasing the source for LifeViewer. I started the project in 2012 as a vehicle to learn Javascript/HTML5 and forgot to stop. The commented source is about 25,000 lines of code. Minified and pretty-printed by the browser developer tools you get about 12,500 lines which are at least readable.
If minified code is an issue for Opera then feel free to omit LifeViewer. I was just interested by your extension because I'd never seen one. Nice job, by the way.
OK, that's fair enough.

Did you ever talk to Calcyman himself about adding LifeViewer support to Catagolue object pages? (I should perhaps get in touch as well; my ultimate goal would be for the extension to become obsolete because everything it does has been integrated into the site proper.)

And thank you. :)
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Catagolue browser extension

Post by Scorbie » July 15th, 2016, 1:38 am

Currently in the eye of the hurricane of tests. Reporting success with Greasemonkey on Firefox which isn't too surprising. Two trivial issues which doesn't harm usablility:
1. Firefox doesn't convert catagolue.appspot.com/object/xp15_4r4z4r4/b3s23 (for example) to https, which makes the script unlaunched. Changing the URL to *catagolue.appspot.com/object/* in the script works but looks a little ugly. No need to change this, just use https!
EDIT: The popup window doesn't work anyway, so I think we should just use https.
2. The popup window is too large in a laptop screen. Cannot control it unless I zoom out....
Attachments
Large popup window
Large popup window
(1).png (77.36 KiB) Viewed 24982 times

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » July 15th, 2016, 6:22 am

Scorbie wrote:Currently in the eye of the hurricane of tests. Reporting success with Greasemonkey on Firefox which isn't too surprising. Two trivial issues which doesn't harm usablility:
1. Firefox doesn't convert catagolue.appspot.com/object/xp15_4r4z4r4/b3s23 (for example) to https, which makes the script unlaunched. Changing the URL to *catagolue.appspot.com/object/* in the script works but looks a little ugly. No need to change this, just use https!
EDIT: The popup window doesn't work anyway, so I think we should just use https.
These are both easy fixes, actually, so I did go ahead and make those changes. Version 3.5 (below) Works For Me(tm) on both http:// and https://, though obviously I still recommend using https:// .
Scorbie wrote:2. The popup window is too large in a laptop screen. Cannot control it unless I zoom out....
This was trickier. The problem is that textarea elements have both a "rows" attribute and the usual CSS styling; the script set "rows" to 34 unconditionally (32 for a symmetric soup + 1 for the RLE header + 1 to avoid a scrollbar), but properties influencing the textarea's height got largely ignored.

I've now implemented a cludge that limits the sample soup overlay's size to a maximum of 65% of the browser window's inner height (i.e. the height of the content, excluding the browser's own UI). This gives you a reasonably-sized overlay on very short browser windows while still giving it room to grow to the full size on larger screens.

Forcing it this way feels ugly to me -- there should be a way to simply tell the element "don't grow beyond your parent" --, but at least it works. (For the record, setting the max-height property to "100% does not.)

Code: Select all

// ==UserScript==
// @name        Catagolue Reloaded
// @namespace   None
// @description Various useful tweaks to Catagolue object pages.
// @include     *://catagolue.appspot.com/object/*
// @version     3.5
// @grant       none
// ==/UserScript==

// "On second thought, let's not hack in Javascript. 'tis a silly language."

// separator used for breadcrumb navigation links
var breadcrumbSeparator = " » "; // > ›

// MAIN function.
function MAIN() {

	// read page parameters
	var params = readParams();

	if(params != null) {

		// do our work.
		addNavLinks    (params);
		handleSampleSoups(params);
		objectToRLE    (params);

	} else {

		// this shouldn't happen on pages where this script actually runs.
		console.log("Could not read page parameters.");

	}
}

/*********************************
 * HTML-related helper functions *
 *********************************/

// find the heading containing the object's code
function findTitleHeading() {

	// find the content div; the heading is (currently) its first child.
	var content = document.getElementById("content");
	if(content)
		return content.firstElementChild;

	// this shouldn't happen unless the page layout changes.
	return null;

}

// find the H2 beginning the comments section.
function findCommentsH2() {

	var contentRegex = /Comments \(/;

	// most elements on Catagolue pages do not have ids etc., so instead we 
	// look for the right h2 tag.
	// NOTE: this may break if Catagolue's page layout changes.
	var h2s = document.getElementsByTagName("h2");
	for(var i = 0; i < h2s.length; i++) {
	  
		if(contentRegex.test(h2s[i].textContent)) {
			return h2s[i];
		}
	}

	// not found?
	return null;

}

// find the paragraph containing the sample soup links.
function findSampleSoupsParagraph() {

	// most elements on Catagolue pages do not have ids etc., so instead we
	// look for the right h3 tag; the paragraph we want is the following 
	// element.
	// NOTE: this may break if Catagolue's page layout changes.
	var h3s = document.getElementsByTagName("h3");
	for(var i = 0; i < h3s.length; i++) {
	  
		if(h3s[i].textContent == "Sample occurrences") {
			return h3s[i].nextElementSibling;
		}
	}

	// not found?
	return null;

}

// append a table row containing a hr element to a node (a table, in practice).
function appendHR(node) {

	var hrRow  = document.createElement("tr");
	var hrCell = document.createElement("td");
	var hr     = document.createElement("hr");

	// HACK: hardcoded colspan=3.
	hrCell.colSpan             = "3";
	hrCell.style.paddingTop    = "0";
	hrCell.style.paddingBottom = "0"
    hr.    style.margin        = "0";


	node.  appendChild(hrRow);
	hrRow. appendChild(hrCell);
	hrCell.appendChild(hr);

}

// read and return apgcode, rulestring etc.
function readParams() {

	// regular expression to extract apgcode and rulestring from the page URL
	var locRegex = /object\/(.*?)\/(.*)/;

	// regular expression to extract prefix and encoded object from apgcode
	var codeRegex = /^(.*?)_(.*)$/;

	var matches = locRegex.exec(document.location);
	if(matches) {

		var params = new Object;

		// parameters extracted from URL go here.

		params["apgcode"] = matches[1];

		// the rulestring may or may not contain the symmetry as well.
		// Normally it won't, but if the user came from a page that our
		// symmetry injector script ran on, it will.
		if(matches[2].indexOf("/") == -1) {

			params["rule"    ] = matches[2];
			params["symmetry"] = null;

		} else {

			var pieces = matches[2].split("/", 2);

			params["rule"    ] = pieces[0];
			params["symmetry"] = pieces[1];

		}

		// pathologicals do not have an object code apart from the prefix
		// itself.
		if(matches[1] == "PATHOLOGICAL") {

			params["prefix"] = "PATHOLOGICAL";
			params["object"] = "";

		} else {

			// separate prefix from code proper.
			// FIXME: shouldn't simply assume this succeeds, I guess.
			var pieces = codeRegex.exec(matches[1]);

			params["prefix" ] = pieces[1];
			params["object" ] = pieces[2];

		}

		// other parameters go here.
		// (none yet)

		return params;
	}

	// location didn't match.
	return null;

}

// create and return a hyperlink
function makeLink(linkTarget, linkText) {

	// create a new "a" element
	var link = document.createElement("a");

	link.href        = linkTarget;
	link.textContent = linkText;

	return link;

}

/****************************************
 * General GoL-related helper functions *
 ****************************************/

// return an empty universe of the desired size
function emptyUniverse(bx, by) {

	// there's no autovivification.
	var universe = new Array(bx);
	for (var i = 0; i < bx; i++) {
		universe[i] = new Array(by);
	}

	return universe;
}

// convert a rulestring to slashed uppercase notation, e.g. "B3/S23" instead
// of "b3s23" etc. Note that named rules (e.g. "tlife") are left alone, as are
// neighborhood conditions in non-totalistic rules in Hensel notation.
function ruleSlashedUpper(rule) {

	rule = rule.replace(new RegExp("b", "g"), "B");
	rule = rule.replace(new RegExp("s", "g"), "/S");

	// we may have introduced double slashes.
	rule = rule.replace(new RegExp("//", "g"), "/");

	return rule;

}

// debugging function: return a pattern object as a string, suitable for
// visual inspection (e.g. using console.log).
function patternToString(patternObject) {

	// string to return
	var strPattern = "";

	// read pattern line by line
	for(var i = 0; i <= patternObject["by"]; i++) {
		for(var j = 0; j <= patternObject["bx"]; j++) {

			// live cells are represented by an O, dead ones by a .
			if(patternObject["pattern"][j][i])
				strPattern += "O";
			else
				strPattern += ".";
		}

		// add a linebreak at the end of each pattern line
		strPattern += "\n";
	}

	return strPattern;
}

/************************************
 * apgcode-related helper functions *
 ************************************/

// decode w/x/y in an apgcode.
// FIXME: there's got to be a more elegant/idiomatic way of doing this.
function apgcodeDecodeWXY(code) {

	// replace y0 to y9 with 4 to 13 zeroes, respectively.
	for(var i = 0; i <= 9; i++) {
		code = code.replace(new RegExp("y" + i.toString(), "g"), "0".repeat(i + 4));
	}

	// replace ya to yz with 14 to 39 zeroes, respectively.
	// NOTE: 97=ord('a'); 122=ord('z').
	for(var i = 97; i <= 122; i++) {
		code = code.replace(new RegExp("y" + String.fromCharCode(i), "g"), "0".repeat(i - 83));
	}

	// finally, replace w and x with 2 and 3 zeroes, respectively.
	// NOTE: this needs to come last so yw and yx will be handled correctly.
	code = code.replace(new RegExp("w", "g"), "00");
	code = code.replace(new RegExp("x", "g"), "000");

	return code;

}

// Convert an object (represented by its apgcode) to a pattern.
function apgcodeToPattern(object, rule) {

	// create a 40x40 array to hold the pattern. Note that 40x40 is the 
	// maximum object size;  larger objects are classified as PATHOLOGICAL on
	// Catagolue.
	var pattern = emptyUniverse(40, 40);

	// decode w/x/y
	object = apgcodeDecodeWXY(object);

	// split object's apgcode into strips.
	var strips = object.split("z");

	// bounding box; this is computed en passant.
	var bx = 0;
	var by = 0;

	for(var i = 0; i < strips.length; i++) {

		// split strip into characters.
		var characters = strips[i].split("");

		for(var j = 0; j < characters.length; j++) {
			var charCode = characters[j].charCodeAt(0);

			// decode character. Letters a-v denote numbers 10-31.
			var number = 0;
			if((charCode >= 48) && (charCode <= 57)) {
				number = charCode - 48;
			} else if((charCode >= 97) && (charCode <= 118)) {
				number = charCode - 87;
			}

			// each character encodes five bits.
			for(var bit = 0; bit <= 4; bit++) {

				var x = j;
				var y = i * 5 + bit;

				// If a bit is set...
				if(number & (Math.pow(2, bit))) {

					// take note of bounding box.
					if(x > bx)
						bx = x;

					if(y > by)
						by = y;

					// and set the cell for this bit.
					pattern[x][y] = 1;
				}
			}
		}
	}

	var ret = new Object();

	ret["pattern"] = pattern;
	ret["bx"     ] = bx;
	ret["by"     ] = by;
	ret["rule"   ] = rule;

	return ret;
}

/********************************
 * RLE-related helper functions *
 ********************************/

// return an encoded RLE run.
function RLEAddRun(count, state) {

	var ret = "";

	if(count > 1)
		ret += count.toString();

	// dead cells are encoded as "b", live cells as "o".
	if(state == 1)
		ret += "o";
	else
		ret += "b";

	return ret;
}

// convert a pattern to an RLE string.
function patternToRLE(patternObject) {

	// extract values
	var pattern = patternObject["pattern"];
	var bx      = patternObject["bx"];
	var by      = patternObject["by"];
	var rule    = patternObject["rule"];

	// RLE pattern
	// the first line is a header.
	var RLE = "x = " + (bx + 1) + ", y = " + (by + 1) + ", rule = " + ruleSlashedUpper(rule) + "\n";

	// state of the ongoing run
	var currentState = "NONE";
	var runCount     = 0;

	var currentLine  = "";

	// read pattern linewise
	for(var i = 0; i <= by; i++) {
		for(var j = 0; j <= bx; j++) {

			// current cell we're looking at
			var cell = pattern[j][i];

			// did we change state?
			if(cell != currentState) {

				// if our line's getting too long, flush it.
				// FIXME: this may actually produce lines slightly longer 
				// than 70 chars. Not a problem in practice, but strictly
				// speaking a violation of the spec.
				if(currentLine.length >= 70) {
					RLE         += currentLine + "\n";
					currentLine  = "";
				}

				// if we have an ongoing run, wrap that up.
				if(currentState != "NONE")
					currentLine += RLEAddRun(runCount, currentState);

				// begin a new run
				currentState = cell;
				runCount     = 1;

			} else
				// continue ongoing run
				runCount++;

		}

		// wrap up current run.
		currentLine += RLEAddRun(runCount, currentState);

		// reset run.
		runCount     = 0;
		currentState = "NONE";

		// if this isn't the last line, begin a new one.
		if(i < by)
			currentLine += "$";
	}

	// wrap up RLE
	RLE += currentLine + "!\n";

	return RLE;

}

/***********************
 * Major functionality *
 ***********************/

/*** INJECTED MD5 SCRIPT ***/

/***************************************************************************
 * NOTE: all code in this script was written by Paul Johnson and is taken  *
 * from http://pajhome.org.uk/crypt/md5/md5.html . The code is licensed    *
 * under the 3-clause BSD license, which is compatible with the GNU GPL.   *
 * See http://pajhome.org.uk/site/legal.html#bsdlicense , as well as the   *
 * FSF's https://www.gnu.org/licenses/license-list.html#ModifiedBSD .      *
 ***************************************************************************/

var MD5Script = `

// change this in case Catagolue moves.
var catagolueHostName  = "catagolue.appspot.com";

/*
 * A JavaScript implementation of the RSA Data Security, Inc. MD5 Message
 * Digest Algorithm, as defined in RFC 1321.
 * Version 2.2 Copyright (C) Paul Johnston 1999 - 2009
 * Other contributors: Greg Holt, Andrew Kepert, Ydnar, Lostinet
 * Distributed under the BSD License
 * See http://pajhome.org.uk/crypt/md5 for more info.
 */

/*
 * Configurable variables. You may need to tweak these to be compatible with
 * the server-side, but the defaults work in most cases.
 */
var hexcase = 0;   /* hex output format. 0 - lowercase; 1 - uppercase        */
var b64pad  = "";  /* base-64 pad character. "=" for strict RFC compliance   */

/*
 * These are the functions you'll usually want to call
 * They take string arguments and return either hex or base-64 encoded strings
 */
function hex_md5(s)    { return rstr2hex(rstr_md5(str2rstr_utf8(s))); }
function b64_md5(s)    { return rstr2b64(rstr_md5(str2rstr_utf8(s))); }
function any_md5(s, e) { return rstr2any(rstr_md5(str2rstr_utf8(s)), e); }
function hex_hmac_md5(k, d)
  { return rstr2hex(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
function b64_hmac_md5(k, d)
  { return rstr2b64(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d))); }
function any_hmac_md5(k, d, e)
  { return rstr2any(rstr_hmac_md5(str2rstr_utf8(k), str2rstr_utf8(d)), e); }

/*
 * Perform a simple self-test to see if the VM is working
 */
function md5_vm_test()
{
  return hex_md5("abc").toLowerCase() == "900150983cd24fb0d6963f7d28e17f72";
}

/*
 * Calculate the MD5 of a raw string
 */
function rstr_md5(s)
{
  return binl2rstr(binl_md5(rstr2binl(s), s.length * 8));
}

/*
 * Calculate the HMAC-MD5, of a key and some data (raw strings)
 */
function rstr_hmac_md5(key, data)
{
  var bkey = rstr2binl(key);
  if(bkey.length > 16) bkey = binl_md5(bkey, key.length * 8);

  var ipad = Array(16), opad = Array(16);
  for(var i = 0; i < 16; i++)
  {
    ipad[i] = bkey[i] ^ 0x36363636;
    opad[i] = bkey[i] ^ 0x5C5C5C5C;
  }

  var hash = binl_md5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
  return binl2rstr(binl_md5(opad.concat(hash), 512 + 128));
}

/*
 * Convert a raw string to a hex string
 */
function rstr2hex(input)
{
  try { hexcase } catch(e) { hexcase=0; }
  var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef";
  var output = "";
  var x;
  for(var i = 0; i < input.length; i++)
  {
    x = input.charCodeAt(i);
    output += hex_tab.charAt((x >>> 4) & 0x0F)
           +  hex_tab.charAt( x        & 0x0F);
  }
  return output;
}

/*
 * Convert a raw string to a base-64 string
 */
function rstr2b64(input)
{
  try { b64pad } catch(e) { b64pad=''; }
  var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  var output = "";
  var len = input.length;
  for(var i = 0; i < len; i += 3)
  {
    var triplet = (input.charCodeAt(i) << 16)
                | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0)
                | (i + 2 < len ? input.charCodeAt(i+2)      : 0);
    for(var j = 0; j < 4; j++)
    {
      if(i * 8 + j * 6 > input.length * 8) output += b64pad;
      else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F);
    }
  }
  return output;
}

/*
 * Convert a raw string to an arbitrary string encoding
 */
function rstr2any(input, encoding)
{
  var divisor = encoding.length;
  var i, j, q, x, quotient;

  /* Convert to an array of 16-bit big-endian values, forming the dividend */
  var dividend = Array(Math.ceil(input.length / 2));
  for(i = 0; i < dividend.length; i++)
  {
    dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1);
  }

  /*
   * Repeatedly perform a long division. The binary array forms the dividend,
   * the length of the encoding is the divisor. Once computed, the quotient
   * forms the dividend for the next step. All remainders are stored for later
   * use.
   */
  var full_length = Math.ceil(input.length * 8 /
                                    (Math.log(encoding.length) / Math.log(2)));
  var remainders = Array(full_length);
  for(j = 0; j < full_length; j++)
  {
    quotient = Array();
    x = 0;
    for(i = 0; i < dividend.length; i++)
    {
      x = (x << 16) + dividend[i];
      q = Math.floor(x / divisor);
      x -= q * divisor;
      if(quotient.length > 0 || q > 0)
        quotient[quotient.length] = q;
    }
    remainders[j] = x;
    dividend = quotient;
  }

  /* Convert the remainders to the output string */
  var output = "";
  for(i = remainders.length - 1; i >= 0; i--)
    output += encoding.charAt(remainders[i]);

  return output;
}

/*
 * Encode a string as utf-8.
 * For efficiency, this assumes the input is valid utf-16.
 */
function str2rstr_utf8(input)
{
  var output = "";
  var i = -1;
  var x, y;

  while(++i < input.length)
  {
    /* Decode utf-16 surrogate pairs */
    x = input.charCodeAt(i);
    y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0;
    if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF)
    {
      x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF);
      i++;
    }

    /* Encode output as utf-8 */
    if(x <= 0x7F)
      output += String.fromCharCode(x);
    else if(x <= 0x7FF)
      output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F),
                                    0x80 | ( x         & 0x3F));
    else if(x <= 0xFFFF)
      output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F),
                                    0x80 | ((x >>> 6 ) & 0x3F),
                                    0x80 | ( x         & 0x3F));
    else if(x <= 0x1FFFFF)
      output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07),
                                    0x80 | ((x >>> 12) & 0x3F),
                                    0x80 | ((x >>> 6 ) & 0x3F),
                                    0x80 | ( x         & 0x3F));
  }
  return output;
}

/*
 * Encode a string as utf-16
 */
function str2rstr_utf16le(input)
{
  var output = "";
  for(var i = 0; i < input.length; i++)
    output += String.fromCharCode( input.charCodeAt(i)        & 0xFF,
                                  (input.charCodeAt(i) >>> 8) & 0xFF);
  return output;
}

function str2rstr_utf16be(input)
{
  var output = "";
  for(var i = 0; i < input.length; i++)
    output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF,
                                   input.charCodeAt(i)        & 0xFF);
  return output;
}

/*
 * Convert a raw string to an array of little-endian words
 * Characters >255 have their high-byte silently ignored.
 */
function rstr2binl(input)
{
  var output = Array(input.length >> 2);
  for(var i = 0; i < output.length; i++)
    output[i] = 0;
  for(var i = 0; i < input.length * 8; i += 8)
    output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (i%32);
  return output;
}

/*
 * Convert an array of little-endian words to a string
 */
function binl2rstr(input)
{
  var output = "";
  for(var i = 0; i < input.length * 32; i += 8)
    output += String.fromCharCode((input[i>>5] >>> (i % 32)) & 0xFF);
  return output;
}

/*
 * Calculate the MD5 of an array of little-endian words, and a bit length.
 */
function binl_md5(x, len)
{
  /* append padding */
  x[len >> 5] |= 0x80 << ((len) % 32);
  x[(((len + 64) >>> 9) << 4) + 14] = len;

  var a =  1732584193;
  var b = -271733879;
  var c = -1732584194;
  var d =  271733878;

  for(var i = 0; i < x.length; i += 16)
  {
    var olda = a;
    var oldb = b;
    var oldc = c;
    var oldd = d;

    a = md5_ff(a, b, c, d, x[i+ 0], 7 , -680876936);
    d = md5_ff(d, a, b, c, x[i+ 1], 12, -389564586);
    c = md5_ff(c, d, a, b, x[i+ 2], 17,  606105819);
    b = md5_ff(b, c, d, a, x[i+ 3], 22, -1044525330);
    a = md5_ff(a, b, c, d, x[i+ 4], 7 , -176418897);
    d = md5_ff(d, a, b, c, x[i+ 5], 12,  1200080426);
    c = md5_ff(c, d, a, b, x[i+ 6], 17, -1473231341);
    b = md5_ff(b, c, d, a, x[i+ 7], 22, -45705983);
    a = md5_ff(a, b, c, d, x[i+ 8], 7 ,  1770035416);
    d = md5_ff(d, a, b, c, x[i+ 9], 12, -1958414417);
    c = md5_ff(c, d, a, b, x[i+10], 17, -42063);
    b = md5_ff(b, c, d, a, x[i+11], 22, -1990404162);
    a = md5_ff(a, b, c, d, x[i+12], 7 ,  1804603682);
    d = md5_ff(d, a, b, c, x[i+13], 12, -40341101);
    c = md5_ff(c, d, a, b, x[i+14], 17, -1502002290);
    b = md5_ff(b, c, d, a, x[i+15], 22,  1236535329);

    a = md5_gg(a, b, c, d, x[i+ 1], 5 , -165796510);
    d = md5_gg(d, a, b, c, x[i+ 6], 9 , -1069501632);
    c = md5_gg(c, d, a, b, x[i+11], 14,  643717713);
    b = md5_gg(b, c, d, a, x[i+ 0], 20, -373897302);
    a = md5_gg(a, b, c, d, x[i+ 5], 5 , -701558691);
    d = md5_gg(d, a, b, c, x[i+10], 9 ,  38016083);
    c = md5_gg(c, d, a, b, x[i+15], 14, -660478335);
    b = md5_gg(b, c, d, a, x[i+ 4], 20, -405537848);
    a = md5_gg(a, b, c, d, x[i+ 9], 5 ,  568446438);
    d = md5_gg(d, a, b, c, x[i+14], 9 , -1019803690);
    c = md5_gg(c, d, a, b, x[i+ 3], 14, -187363961);
    b = md5_gg(b, c, d, a, x[i+ 8], 20,  1163531501);
    a = md5_gg(a, b, c, d, x[i+13], 5 , -1444681467);
    d = md5_gg(d, a, b, c, x[i+ 2], 9 , -51403784);
    c = md5_gg(c, d, a, b, x[i+ 7], 14,  1735328473);
    b = md5_gg(b, c, d, a, x[i+12], 20, -1926607734);

    a = md5_hh(a, b, c, d, x[i+ 5], 4 , -378558);
    d = md5_hh(d, a, b, c, x[i+ 8], 11, -2022574463);
    c = md5_hh(c, d, a, b, x[i+11], 16,  1839030562);
    b = md5_hh(b, c, d, a, x[i+14], 23, -35309556);
    a = md5_hh(a, b, c, d, x[i+ 1], 4 , -1530992060);
    d = md5_hh(d, a, b, c, x[i+ 4], 11,  1272893353);
    c = md5_hh(c, d, a, b, x[i+ 7], 16, -155497632);
    b = md5_hh(b, c, d, a, x[i+10], 23, -1094730640);
    a = md5_hh(a, b, c, d, x[i+13], 4 ,  681279174);
    d = md5_hh(d, a, b, c, x[i+ 0], 11, -358537222);
    c = md5_hh(c, d, a, b, x[i+ 3], 16, -722521979);
    b = md5_hh(b, c, d, a, x[i+ 6], 23,  76029189);
    a = md5_hh(a, b, c, d, x[i+ 9], 4 , -640364487);
    d = md5_hh(d, a, b, c, x[i+12], 11, -421815835);
    c = md5_hh(c, d, a, b, x[i+15], 16,  530742520);
    b = md5_hh(b, c, d, a, x[i+ 2], 23, -995338651);

    a = md5_ii(a, b, c, d, x[i+ 0], 6 , -198630844);
    d = md5_ii(d, a, b, c, x[i+ 7], 10,  1126891415);
    c = md5_ii(c, d, a, b, x[i+14], 15, -1416354905);
    b = md5_ii(b, c, d, a, x[i+ 5], 21, -57434055);
    a = md5_ii(a, b, c, d, x[i+12], 6 ,  1700485571);
    d = md5_ii(d, a, b, c, x[i+ 3], 10, -1894986606);
    c = md5_ii(c, d, a, b, x[i+10], 15, -1051523);
    b = md5_ii(b, c, d, a, x[i+ 1], 21, -2054922799);
    a = md5_ii(a, b, c, d, x[i+ 8], 6 ,  1873313359);
    d = md5_ii(d, a, b, c, x[i+15], 10, -30611744);
    c = md5_ii(c, d, a, b, x[i+ 6], 15, -1560198380);
    b = md5_ii(b, c, d, a, x[i+13], 21,  1309151649);
    a = md5_ii(a, b, c, d, x[i+ 4], 6 , -145523070);
    d = md5_ii(d, a, b, c, x[i+11], 10, -1120210379);
    c = md5_ii(c, d, a, b, x[i+ 2], 15,  718787259);
    b = md5_ii(b, c, d, a, x[i+ 9], 21, -343485551);

    a = safe_add(a, olda);
    b = safe_add(b, oldb);
    c = safe_add(c, oldc);
    d = safe_add(d, oldd);
  }
  return Array(a, b, c, d);
}

/*
 * These functions implement the four basic operations the algorithm uses.
 */
function md5_cmn(q, a, b, x, s, t)
{
  return safe_add(bit_rol(safe_add(safe_add(a, q), safe_add(x, t)), s),b);
}
function md5_ff(a, b, c, d, x, s, t)
{
  return md5_cmn((b & c) | ((~b) & d), a, b, x, s, t);
}
function md5_gg(a, b, c, d, x, s, t)
{
  return md5_cmn((b & d) | (c & (~d)), a, b, x, s, t);
}
function md5_hh(a, b, c, d, x, s, t)
{
  return md5_cmn(b ^ c ^ d, a, b, x, s, t);
}
function md5_ii(a, b, c, d, x, s, t)
{
  return md5_cmn(c ^ (b | (~d)), a, b, x, s, t);
}

/*
 * Add integers, wrapping at 2^32. This uses 16-bit operations internally
 * to work around bugs in some JS interpreters.
 */
function safe_add(x, y)
{
  var lsw = (x & 0xFFFF) + (y & 0xFFFF);
  var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
  return (msw << 16) | (lsw & 0xFFFF);
}

/*
 * Bitwise rotate a 32-bit number to the left.
 */
function bit_rol(num, cnt)
{
  return (num << cnt) | (num >>> (32 - cnt));
}

`

/*** End of Paul Johnson's MD5 code ***/

/*** ELEMENT DRAG AND DROP SUPPORT SCRIPT ***/

/*****************************************************************************
 * NOTE: script taken from http://www.quirksmode.org/js/dragdrop.html , with *
 * the following changes:                                                    *
 *     a) removed keyboard dragging.                                         *
 *     b) added ability to ignore mouse clicks in a specified child.         *
 * Event handlers taken from http://www.quirksmode.org/js/eventSimple.html . *
 * Both written by ppk Peter-Paul Koch; no license given, but implied to be  *
 * permissive.                                                               *
 *****************************************************************************/

var dragAndDropSupportScript = `

function addEventSimple(obj,evt,fn) {
	if (obj.addEventListener)
		obj.addEventListener(evt,fn,false);
	else if (obj.attachEvent)
		obj.attachEvent('on'+evt,fn);
}

function removeEventSimple(obj,evt,fn) {
	if (obj.removeEventListener)
		obj.removeEventListener(evt,fn,false);
	else if (obj.detachEvent)
		obj.detachEvent('on'+evt,fn);
}

dragDrop = {
	initialMouseX: undefined,
	initialMouseY: undefined,
	startX: undefined,
	startY: undefined,
	draggedObject: undefined,
	excludedObject: undefined,
	initElement: function (element, excl) {
		if (typeof element == 'string')
			element = document.getElementById(element);
		if (typeof excl == 'string')
			excl = document.getElementById(excl);
		element.onmousedown = dragDrop.startDragMouse;
		excludedObject = excl;
	},
	startDragMouse: function (e) {
		if(e.target == excludedObject)
			return true;
		dragDrop.startDrag(this);
		var evt = e || window.event;
		dragDrop.initialMouseX = evt.clientX;
		dragDrop.initialMouseY = evt.clientY;
		addEventSimple(document,'mousemove',dragDrop.dragMouse);
		addEventSimple(document,'mouseup',dragDrop.releaseElement);
		return false;
	},
	startDrag: function (obj) {
		if (dragDrop.draggedObject)
			dragDrop.releaseElement();
		dragDrop.startX = obj.offsetLeft;
		dragDrop.startY = obj.offsetTop;
		dragDrop.draggedObject = obj;
		obj.className += ' dragged';
	},
	dragMouse: function (e) {
		var evt = e || window.event;
		var dX = evt.clientX - dragDrop.initialMouseX;
		var dY = evt.clientY - dragDrop.initialMouseY;
		dragDrop.setPosition(dX,dY);
		return false;
	},
	setPosition: function (dx,dy) {
		dragDrop.draggedObject.style.left = dragDrop.startX + dx + 'px';
		dragDrop.draggedObject.style.top = dragDrop.startY + dy + 'px';
	},
	releaseElement: function() {
		removeEventSimple(document,'mousemove',dragDrop.dragMouse);
		removeEventSimple(document,'mouseup',dragDrop.releaseElement);
		dragDrop.draggedObject.className = dragDrop.draggedObject.className.replace(/dragged/,'');
		dragDrop.draggedObject = null;
	}
}

`

/*** INJECTED SAMPLE SOUP OVERLAY SCRIPT ***/

var sampleSoupOverlayScript = `

// Event handler to close sample soup overlay. Based on (with modifications)
// https://stackoverflow.com/posts/3369743/revisions .
function closeSoupOverlay(evt) {

    evt          = evt || window.event;
    var isEscape = false;
    var isClick  = false;

    if("key" in evt)
        isEscape = (evt.key == "Escape");
    else
        isEscape = (evt.keyCode == 27);

    if("type" in evt)
        isClick = (evt.type == "mousedown");

    if(isEscape || isClick) {
        var overlayDiv = document.getElementById("sampleSoupOverlay");
        if(overlayDiv)
            overlayDiv.parentNode.removeChild(overlayDiv);
    }

}

function overlaySoup(soupURL, soupNumber, totalSoups) {

	// regex to extract soup seed etc. from soupURL
	// FIXME: using \d instead of [0-9] does not work. Why?
	var soupURLRegex = new RegExp("^(https?://)" + catagolueHostName + "/hashsoup/(.*?)/((m_|n_)?[A-Za-z0-9]{12})([0-9]*)/(.*)$");

	// match regex against soup URL. This also verifies that we're not getting
	// passed just any URL to load remotely.
	var matches = soupURLRegex.exec(soupURL);

	if(!matches) {
		// URL could not be parsed
		console.log("Could not parse soup URL: " + soupURL);
		return false;
	}

	// collect soup parameters.
	// NOTE: seedPrefix would indicate if the haul was submitted using 
	// apgsearch 0.x/1.x (empty string), apgnano 2.x ("n_") or apgmera 3.x
	// ("m_"), but we have no use for this at the moment.
	var URLScheme        = matches[1]
	var symmetry         = matches[2];
	var seed             = matches[3];
	// var seedPrefix       = matches[4];
	var soupNumberInHaul = matches[5];
	var rule             = matches[6];

	// URL for the haul containing this soup.
	var haulURL = URLScheme + catagolueHostName + "/haul/" + rule + "/" + symmetry + "/" + hex_md5(seed);

    var color = symmetryColors[symmetry] || "black";

	// Sample soup overlay, based on (with modifications) Method 5 on
	// http://www.vikaskbh.com/five-css-techniques-make-overlay-div-centered/

	// create the elements we'll need.
	var overlayDiv         = document.createElement("div");
    var overlayInnerDiv    = document.createElement("div");
    var overlayShadingDiv  = document.createElement("div");
    var introParagraph     = document.createElement("p");
	var haulLink           = document.createElement("a");
	var soupSelectAll      = document.createElement("p");
	var soupSelectAllLink  = document.createElement("a");
	var sampleSoupTextarea = document.createElement("textarea");

	// style outer div.
    overlayDiv.id             = "sampleSoupOverlay";
    overlayDiv.style.position = "fixed";
    overlayDiv.style.top      = "0";
    overlayDiv.style.left     = "0";
    overlayDiv.style.width    = "100%";
    overlayDiv.style.zIndex   = "1";
    overlayDiv.style.height   = "100%";

	// style inner div.
	// NOTE: margin-left must be half of width for the div to be centered.
	// NOTE 2: border color matches the color of the sample soup dots for
	// this symmetry.
	// NOTE 3: #003040 is a shade of deep cerulean, taken from Catagolue's
	// background image.
    overlayInnerDiv.style.position        = "relative";
    overlayInnerDiv.style.left            = "50%";
    overlayInnerDiv.style.marginLeft      = "-375px";
    overlayInnerDiv.style.border          = "5px outset " + color;
    overlayInnerDiv.style.borderRadius    = "10px";
    overlayInnerDiv.style.backgroundColor = "white";
    overlayInnerDiv.style.color           = "black";
    overlayInnerDiv.style.width           = "750px";
    overlayInnerDiv.style.zIndex          = "3";
    overlayInnerDiv.style.top             = "20%";
    overlayInnerDiv.style.minHeight       = "50%";
	overlayInnerDiv.style.maxHeight       = "90%";
    overlayInnerDiv.style.padding         = "1em";
    overlayInnerDiv.style.boxShadow       = "10px -10px 10px 0px #003040";

	// style the div that will shade the remainder of the page behind the
	// sample soup while the overlay is open.
    overlayShadingDiv.style.position        = "fixed";
    overlayShadingDiv.style.width           = "100%";
    overlayShadingDiv.style.height          = "100%";
    overlayShadingDiv.style.margin          = "auto";
    overlayShadingDiv.style.backgroundColor = "black";
    overlayShadingDiv.style.opacity         = "0.5";
    overlayShadingDiv.style.zIndex          = "2";
    overlayShadingDiv.style.top             = "0";
    overlayShadingDiv.style.left            = "0";

	// clicking outside the overlay will close it.
    overlayShadingDiv.onmousedown           = closeSoupOverlay;

	// link to the haul containing this soup
	haulLink.href        = haulURL;
	haulLink.textContent = "Haul";
 
	// a short introductory note informing the user which soup this is.
	// NOTE: U+2116 is the "Numero" symbol.
    introParagraph.style.marginTop = "0";
    introParagraph.appendChild(document.createTextNode(symmetry + " soup \u2116 " + soupNumber.toString() + " / " + totalSoups.toString() + " ("));
	introParagraph.appendChild(haulLink);
	introParagraph.appendChild(document.createTextNode(")"));

	// create "select all" link for the sample soup
	soupSelectAll.style.marginTop    = 0;
	soupSelectAll.style.marginBottom = "0.5em";
	soupSelectAll.style.fontFamily   = "monospace";

	soupSelectAllLink.href        = "#";
	soupSelectAllLink.textContent = "Select All";
	soupSelectAllLink.setAttribute("onclick", 'document.getElementById("sampleSoupTextArea").select(); return false');

	soupSelectAll.appendChild(soupSelectAllLink);

	// the textarea that will hold the soup.
    sampleSoupTextarea.id              = "sampleSoupTextArea";
    sampleSoupTextarea.style.width     = "100%";
	sampleSoupTextarea.style.overflowY = "scroll";
    sampleSoupTextarea.rows            = "34";
	sampleSoupTextarea.style.maxHeight = (window.innerHeight * 0.65) + "px";
    sampleSoupTextarea.readOnly        = true;
    sampleSoupTextarea.textContent     = "Loading " + soupURL + ", please wait...";

	// assemble elements.
    document.getElementById("sampleSoupTable").appendChild(overlayDiv);
    overlayDiv.appendChild(overlayInnerDiv);
    overlayDiv.appendChild(overlayShadingDiv);
    overlayInnerDiv.appendChild(introParagraph);
    overlayInnerDiv.appendChild(soupSelectAll);
    overlayInnerDiv.appendChild(sampleSoupTextarea);

	// make sample soup overlay draggable with the mouse; text area is
	// excluded (so the user can select the sample soup with the mouse).
	dragDrop.initElement("sampleSoupOverlay", "sampleSoupTextArea");

	// asynchronous request to retrieve the soup.
    var sampleSoupRequest = new XMLHttpRequest();

	// once the soup is loaded, put it into the textarea.
    sampleSoupRequest.addEventListener("load", function() {
            var sampleSoupTextarea = document.getElementById("sampleSoupTextArea");
            if(sampleSoupTextarea)
                sampleSoupTextarea.textContent = sampleSoupRequest.responseText;
        });

	// fire off request.
    sampleSoupRequest.open("GET", soupURL);
    sampleSoupRequest.send();

	// success!
	return true;

}

// colors used for the various symmetries. Color values are probably
// autogenerated from the symmetries' names, but I don't know how, so here's
// a hardcoded list.
var symmetryColors = new Object();

// standard symmetries. 
symmetryColors["25pct"] = "#72da55";
symmetryColors["75pct"] = "#10963a";
symmetryColors["8x32" ] = "#6d0ecf";
symmetryColors["C1"   ] = "black";
symmetryColors["C2_1" ] = "#f83e05";
symmetryColors["C2_2" ] = "#31a6d8";
symmetryColors["C2_4" ] = "#aceb02";
symmetryColors["C4_1" ] = "#d085ff";
symmetryColors["C4_4" ] = "#cd14a0";
symmetryColors["D2_+1"] = "#39bab9";
symmetryColors["D2_+2"] = "#747d16";
symmetryColors["D2_x" ] = "#fb71fe";
symmetryColors["D4_+1"] = "#f6b2b6";
symmetryColors["D4_+2"] = "#f8e612";
symmetryColors["D4_+4"] = "#cfc20e";
symmetryColors["D4_x1"] = "#ae360f";
symmetryColors["D4_x4"] = "#3e5b59";
symmetryColors["D8_1" ] = "#ed65b6";
symmetryColors["D8_4" ] = "#a621fb";

// "weird" symmetries
symmetryColors["D4 +4"] = "#d32f3f";
symmetryColors["D8_+4"] = "#0bb2a2";

// register an event handler so the soup overlay can be closed by pressing escape.
document.onkeydown = closeSoupOverlay;
`;

/*** INJECTED SCRIPT ENDS ***/

// inject a function to display sample soups in an overlay.
function injectScript(injectedScript) {

	// create a new script element
	var script = document.createElement("script");
	script.type = "text/javascript";

	// inject script text
	script.textContent = injectedScript;

	// append script to document
	document.getElementsByTagName("head")[0].appendChild(script);

}

// sort the sample soups on a Catagolue object page by symmetry.
function handleSampleSoups(params) {

	// regular expression to extract symmetries from sample soup links
	var symRegex   = /hashsoup\/(.*?)\/.*?\/(.*?)$/;

	// hash of arrays containing sample soup links, grouped by symmetry
	var soupLinks  = new Object();

	// total number of sample soups
	var totalSoups = 0;
  
	// paragraph holding the sample soups.
	var sampleSoupsParagraph = findSampleSoupsParagraph();

	// parse links on this page, and convert HTMLCollection to an array so it 
	// won't be "live" and change underneath us when we remove those links.
	var links = Array.prototype.slice.call(sampleSoupsParagraph.getElementsByTagName("a"));

	// we want to have soup links pop up an overlay with a textarea. In order 
	// to do this, we set an onclick handler on the links below that calls a
	// function doing this. This function must live in the document, however,
	// so we inject it now.
	injectScript(sampleSoupOverlayScript);

	// furthermore, we need to inject Paul Johnston's MD5 script, since
	// Javascript lacks any built-in support for computing MD5 hashes.
	injectScript(MD5Script);

	// finally, we need to insert Peter-Paul Koch's element dragging script,
	// so that the soup overlay can be dragged around the page with the mouse.
	injectScript(dragAndDropSupportScript);

	for(var i = 0; i < links.length; i++) {
	  
		var link	   = links[i];
		var linkTarget = link.getAttribute("href");
	  
		var matches	= symRegex.exec(linkTarget);
		if(matches) {

			// there's no autovivification, sigh.
			if(!soupLinks[matches[1]]) {
			  soupLinks[matches[1]] = [];
			}

			totalSoups++;
			soupLinks[matches[1]].push(link);
			link.remove();
		  
		}
	}

	// now that all the links are collected and removed, add a table.
	var table = document.createElement("table");
	table.id                    = "sampleSoupTable";
	table.style.backgroundColor = "#a0ddcc";
	table.style.border          = "2px solid";
	table.style.borderRadius    = "10px";
	table.style.width           = "100%";

	// add table headers.
	var headerRow = document.createElement("tr");
	table.appendChild(headerRow);
  
	var header1 = document.createElement("th");
	header1.textContent = "Symmetry";
	headerRow.appendChild(header1);
  
	var header2 = document.createElement("th");
	header2.innerHTML = "#&nbsp;Soups";
	headerRow.appendChild(header2);
  
	var header3 = document.createElement("th");
	header3.textContent = "Sample soup links";
	headerRow.appendChild(header3);

	// insert table into page, replacing the old sample soup paragraph.
	sampleSoupsParagraph.parentNode.replaceChild(table, sampleSoupsParagraph);

	// iterate through symmetries and add new links.
	var symmetries = Object.keys(soupLinks).sort();
	for(var i = 0; i < symmetries.length; i++) {

		var symmetry = symmetries[i];
		var numSoups = soupLinks[symmetry].length;

		// add a hr between table rows, for the sake of looks.
		appendHR(table);

		// create a new row holding the soup links for this symmetry.
		var tableRow = document.createElement("tr");
		table.appendChild(tableRow);

		// create a table cell indicating the symmetry.
		var tableCell1 = document.createElement("td");
		tableRow.appendChild(tableCell1);

		// create a link to the main census page for this rulesym.
		var censusLink = document.createElement("a");
		censusLink.href        = "/census/" + params["rule"] + "/" + symmetry;
		censusLink.textContent = symmetry;
		tableCell1.appendChild(censusLink);

		// create a table cell indicating the number of sample soup.
		var tableCell2 = document.createElement("td");
		tableCell2.textContent = numSoups;
		tableRow.appendChild(tableCell2);

		// create a table cell holding the sample soup links.
		var tableCell3 = document.createElement("td");
		for(var j = 0; j < numSoups; j++) {

			var link = soupLinks[symmetry][j];

			// modify link so that when the user's browsing with Javascript
			// enabled, clicking it pops up an overlay with the sample soup
			// in a textarea.
			// NOTE: returning false here keeps the link's href from being
			// loaded after the function has run. Note further that returning
			// false FROM the function does not work.
			link.setAttribute("onclick", 'return !overlaySoup("' + link.href + '", ' + (j + 1).toString() + ', ' + numSoups.toString() + ')');

			// put link in this table cell.
			tableCell3.appendChild(link);
			tableCell3.appendChild(document.createTextNode(" "));

		}
		tableRow.appendChild(tableCell3);
	  
	}

	// add another hr before the "totals" row.
	appendHR(table);
 
	// now add a row indicating the total number of sample soups.
	var totalsRow = document.createElement("tr");
	table.appendChild(totalsRow);
  
	var totals1 = document.createElement("th");
	totals1.textContent = "Total";
	totalsRow.appendChild(totals1);
  
	var totals2 = document.createElement("th");
	totals2.innerHTML = totalSoups;
	totalsRow.appendChild(totals2);
  
}

// add a textarea with the object in RLE format.
function objectToRLE(params) {

	var prefix = params["prefix"];
	var object = params["object"];
	var rule   = params["rule"  ];

	// regex to test prefix
	var prefixRegex = /^x[pqs]/;

	// only run for known objects.
	if(object == null)
		return;

	// only run for spaceships (xq), oscillators (xp) and still lifes (xs).
	if(!prefixRegex.test(prefix))
		return;

	// convert object to pattern, and pattern to RLE.
	var pattern = apgcodeToPattern(object, rule);
	var RLE     = patternToRLE    (pattern);

	// find the "Comments" H2
	var commentsH2 = findCommentsH2();

	// create a new heading for the RLE code.
	var RLEHeading = document.createElement("h3");
	RLEHeading.textContent = "RLE";

	// create "select all" link for RLE code.
	var RLESelectAll     = document.createElement("p");
	var RLESelectAllLink = document.createElement("a");

	RLESelectAll.style.marginTop    = 0;
	RLESelectAll.style.marginBottom = "0.5em";
	RLESelectAll.style.fontFamily   = "monospace";

	RLESelectAllLink.href        = "#";
	RLESelectAllLink.textContent = "Select All";
	RLESelectAllLink.setAttribute("onclick", 'document.getElementById("RLETextArea").select(); return false');

	RLESelectAll.appendChild(RLESelectAllLink);

	// create a textarea for the RLE code.
	var RLETextArea = document.createElement("textarea");
	RLETextArea.id          = "RLETextArea";
	RLETextArea.style.width = "100%";
	RLETextArea.rows        = "10";
	RLETextArea.readOnly    = true;
	RLETextArea.textContent = RLE;

	// insert the new nodes.
	commentsH2.parentNode.insertBefore(RLEHeading,   commentsH2);
	commentsH2.parentNode.insertBefore(RLESelectAll, commentsH2);
	commentsH2.parentNode.insertBefore(RLETextArea,  commentsH2);

}

// add navigation
function addNavLinks(params) {

	var rule     = params["rule"];
	var prefix   = params["prefix"];
	var symmetry = params["symmetry"];

	// if symmetry is not set, default to C1.
	if(!symmetry)
		symmetry = "C1";

	// heading containing the object's code
	var titleHeading = findTitleHeading();

	// main content div
	var contentDiv = titleHeading.parentNode;

	// new paragraph for navigation links
	var navigationParagraph = document.createElement("p");

	// insert navigation paragraph before title heading
	contentDiv.insertBefore(navigationParagraph, titleHeading);

	// add breadcrumb links to navigation paragraph
	navigationParagraph.appendChild(document.createTextNode("You are here: "));
	navigationParagraph.appendChild(makeLink("/census/", "Census"));
	navigationParagraph.appendChild(document.createTextNode(breadcrumbSeparator));
	navigationParagraph.appendChild(makeLink("/census/" + rule, rule));
	navigationParagraph.appendChild(document.createTextNode(breadcrumbSeparator));
	navigationParagraph.appendChild(makeLink("/census/" + rule + "/" + symmetry, symmetry));
	navigationParagraph.appendChild(document.createTextNode(breadcrumbSeparator));
	navigationParagraph.appendChild(makeLink("/census/" + rule + "/" + symmetry + "/" + prefix, prefix));

}

// ### MAIN ###
MAIN();
Side note for Opera users: all versions have been submitted to Opera Add-Ons, but none beyond 3.0 have been looked at yet.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Catagolue browser extension

Post by Scorbie » July 16th, 2016, 12:29 am

@Apple Bottom: Wow! Thanks for the work! Just one thing: in order for just plain
http://catagolue.appspot.com/object/xp15_4r4z4r4/b3s23 to work one should slightly edit the @include line to:
*catagolue.appspot.com/object/*
Thanks for all the work. It works nicely on my computer...
̶D̶o̶e̶s̶n̶'̶t̶ ̶w̶o̶r̶k̶ ̶o̶n̶ ̶n̶o̶n̶-̶m̶a̶x̶i̶m̶i̶z̶e̶d̶ ̶s̶c̶r̶e̶e̶n̶s̶,̶ ̶b̶u̶t̶ ̶I̶ ̶a̶m̶ ̶f̶i̶n̶e̶ ̶w̶i̶t̶h̶ ̶t̶h̶a̶t̶ ̶a̶n̶d̶ ̶I̶ ̶h̶a̶v̶e̶ ̶a̶l̶r̶e̶a̶d̶y̶ ̶m̶a̶d̶e̶ ̶y̶o̶u̶ ̶d̶o̶ ̶m̶u̶c̶h̶ ̶w̶o̶r̶k̶.̶

Thanks!

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » July 16th, 2016, 5:31 am

Scorbie wrote:@Apple Bottom: Wow! Thanks for the work! Just one thing: in order for just plain
http://catagolue.appspot.com/object/xp15_4r4z4r4/b3s23 to work one should slightly edit the @include line to:
*catagolue.appspot.com/object/*
Thanks for all the work. It works nicely on my computer...
̶D̶o̶e̶s̶n̶'̶t̶ ̶w̶o̶r̶k̶ ̶o̶n̶ ̶n̶o̶n̶-̶m̶a̶x̶i̶m̶i̶z̶e̶d̶ ̶s̶c̶r̶e̶e̶n̶s̶,̶ ̶b̶u̶t̶ ̶I̶ ̶a̶m̶ ̶f̶i̶n̶e̶ ̶w̶i̶t̶h̶ ̶t̶h̶a̶t̶ ̶a̶n̶d̶ ̶I̶ ̶h̶a̶v̶e̶ ̶a̶l̶r̶e̶a̶d̶y̶ ̶m̶a̶d̶e̶ ̶y̶o̶u̶ ̶d̶o̶ ̶m̶u̶c̶h̶ ̶w̶o̶r̶k̶.̶

Thanks!
Yer welcome, pardner!

It's working fine for me on http://catagolue.appspot.com/object/xp15_4r4z4r4/b3s23, too, BTW. I can make the change, but I'm not sure why it's not working for you as-is.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Scorbie
Posts: 1692
Joined: December 7th, 2013, 1:05 am

Re: Catagolue browser extension

Post by Scorbie » July 16th, 2016, 9:09 am

@Applebottom Whoa... Pardon the misunderstanding all along... I tried to write

Code: Select all

catagolue.appspot.com/...
and (you might have guessed it) phpBB sneakily added the http:// in front. Bad phpBB!
And http works pleasantly. Thanks.

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » July 27th, 2016, 7:39 am

Opera users rejoice: addons.opera.com moderators finally got around to approving the latest version of the extension.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » August 24th, 2016, 8:52 am

I've just submitted version 3.6 to Opera's add-on site (shouldn't be too long before it's approved, I hope). There's two changes in this version, both affecting the main census overview page.
  1. Unordered lists are replaced by ordered ones. (Personal preference of mine.)
  2. The layout doesn't break anymore in the presence of overly long rulestrings such as "b3-ce4-cejnry5es2-cn3-ce4cny5e".
If you're using Firefox or Chrome, there's a new userscript handling these things; just add this, and keep the others in place.

Code: Select all

// ==UserScript==
// @name        Catagolue main census page tweaks
// @namespace   None
// @description Tweaks for Catagolue's main census overview page
// @include     *://catagolue.appspot.com/census
// @include     *://catagolue.appspot.com/census/
// @version     1.0
// @grant       none
// ==/UserScript==

// "On second thought, let's not hack in Javascript. 'tis a silly language."

// MAIN function.
function MAIN() {

	// convert unordered lists to ordered.
	convertLists();

	// adjust content DIV width.
	adjustContentWidth();

}

/*********************************
 * HTML-related helper functions *
 *********************************/

// create a new element based on an existing one (there is no built-in way to 
// simply "convert" an element to a new type).
// Code: "James", https://stackoverflow.com/a/2207198
function makeNewElementFromElement(tag, elem) {

    var newElem = document.createElement(tag);
	var attr    = elem.attributes;
	var attrLen = attr.length;

    // Copy children 
    elem = elem.cloneNode(true);
    while (elem.firstChild) {
        newElem.appendChild(elem.firstChild);
    }

    // Copy DOM properties
    for (var i in elem) {
        try {
            var prop = elem[i];
            if (prop && i !== 'outerHTML' && (typeof prop === 'string' || typeof prop === 'number')) {
                newElem[i] = elem[i];
            }
        } catch(e) { /* some props throw getter errors */ }
    }

    // Copy attributes
    for (var i = 0; i < attrLen; i++) {
        newElem.setAttribute(attr[i].nodeName, attr[i].nodeValue);
    }

    // Copy inline CSS
    newElem.style.cssText = elem.style.cssText;

    return newElem;
}

// adjust an element's width
function adjustElementWidth(element) {

	// get element's computed style
	var elementStyle = window.getComputedStyle(element);

	// set minimum width and unset width
	element.style.minWidth = elementStyle.width;
	element.style.width    = "unset";

}

/***********************
 * Major functionality *
 ***********************/

// convert unordered lists to ordered.
function convertLists() {

	// parse lists on this page
	// , and convert HTMLCollection to an array so it 
	// won't be "live" and change underneath us when we remove those links.
	var lists = Array.prototype.slice.call(document.getElementsByTagName("ul"));
	for(var i = 0; i < lists.length; i++) {

		var list    = lists[i];
		var newList = list.parentNode.replaceChild(makeNewElementFromElement("ol", list), list);

	}

}

// page layout may break if outer-totalistic rulestrings get overly long. To
// prevent this, replace the hard-coded with with a minimum width.
function adjustContentWidth() {

	// find some divs...
	var contentDiv = document.getElementById("content");
	var centerDiv  = document.getElementById("centerDiv");

	// ...and adjust their widths.
	adjustElementWidth(contentDiv);
	adjustElementWidth(centerDiv );
	
	centerDiv.style.display = "inline-block";
	contentDiv.style.padding = "0 19px";

	// there are several navbar elements all sharing the same ID, so using
	// document.getElementById does not work here.
	var navbarDivs = document.querySelectorAll("[id='cl_navbar']");
	for(var i = 0; i < navbarDivs.length; i++)
		adjustElementWidth(navbarDivs[i]);

}

// ### MAIN ###
MAIN();
There's some minor visual anomalies around the top and bottom navbars that I've not been able to figure out yet. If anyone has an idea how to change the styling so it visually matches the original page again, let me know.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » September 18th, 2016, 8:33 am

3.7 is live on addons.opera.com, and adds one-click searches by apgcode to object pages (currently on the LifeWiki, the ConwayLife.com forums, and Google; suggestions for other places to search are welcome).

3.8 has been submitted for approval, and contains several changes. Object pages link to SVG images now, long apgcodes are word-wrapped on object and object category pages (check it out here and here, for example), and RLE generation has been fixed to not choke on patterns exceeding a 40x40 bounding box: the maximum size is now 100x100, and larger objects fail gracefully. Pending moderator approval this version should be available soon, probably tomorrow.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
BlinkerSpawn
Posts: 1992
Joined: November 8th, 2014, 8:48 pm
Location: Getting a snacker from R-Bee's

Re: Catagolue browser extension

Post by BlinkerSpawn » September 24th, 2016, 9:24 pm

Pages that load when a pattern is searched for don't fit the script's required URL format and thus don't call the userscript.

Code: Select all

http://catagolue.appspot.com/object?apgcode=xs10_69ar&rule=b3s2-i34q
Tried adding "http://catagolue.appspot.com/object?apgcode=*" to User Include list in Greasemonkey; presumably that's not how the wildcard character was intended to be used because it doesn't work.
How do I fix this?
LifeWiki: Like Wikipedia but with more spaceships. [citation needed]

Image

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » September 25th, 2016, 7:22 am

BlinkerSpawn wrote:Pages that load when a pattern is searched for don't fit the script's required URL format and thus don't call the userscript.

Code: Select all

http://catagolue.appspot.com/object?apgcode=xs10_69ar&rule=b3s2-i34q
Tried adding "http://catagolue.appspot.com/object?apgcode=*" to User Include list in Greasemonkey; presumably that's not how the wildcard character was intended to be used because it doesn't work.
How do I fix this?
Adding that pattern to the @include list should work. (I think.)

I think this is most likely because the script reads object parameters such as apgcode and rule from the page's URL, but isn't currently equipped to handle the URLs that searching for an object produces. Check your Javascript console; if there's a message saying "Could not read page parameters." from catagoluereloaded.js, that's it.

I'm working on handling these properly, give me a few and I should have something ready.

EDIT: try replacing the readParams function in the script with the following:

Code: Select all

// read and return apgcode, rulestring etc.
function readParams() {

	// regular expression to extract apgcode and rulestring from page URL
	var locRegex  = /object\/(.*?)\/(.*)/;

	// regular expression to extract query string from page URL
	var locRegex2 = /object\?(.*)/;

	// regular expression to extract prefix and encoded object from apgcode
	var codeRegex = /^(.*?)_(.*)$/;

	// hash of extracted page parameters
	var params = new Object;

	// we may not be able to extract/determine the symmetry, so just set it
	// to null explicitely.
	params["symmetry"] = null;

	// parameters extracted from URL go here.
	
	// Handle "regular" locations, e.g.
	// https://catagolue.appspot.com/object/xs4_33/b3s23/C1
	var matches = locRegex.exec(document.location);
	if(matches) {

		params["apgcode"] = matches[1];

		// the rulestring may or may not contain the symmetry as well.
		// Normally it won't, but if the user came from a page that our
		// symmetry injector script ran on, it will.
		if(matches[2].indexOf("/") == -1) {

			params["rule"    ] = matches[2];

		} else {

			var pieces = matches[2].split("/", 2);

			params["rule"    ] = pieces[0];
			params["symmetry"] = pieces[1];

		}

	} else {

		// handle search-based locations, e.g.
		// http://catagolue.appspot.com/object?apgcode=xs10_69ar&rule=b3s2-i34q
		matches = locRegex2.exec(document.location);

		// if neither of these regexes matched, we give up.
		if(!matches)
			return null;

		// query parameters, e.g. "apgcode=xs10_69ar&rule=b3s2-i34q"
		var queryParameterString = matches[1];

		// split these into individual key/value pairs and look at each.
		var queryParameters = queryParameterString.split("&");
		for(var i = 0; i < queryParameters.length; i++) {

			// split this pair into key and value.
			var pieces = queryParameters[i].split("=", 2);

			if(pieces[0] == "apgcode")
				params["apgcode" ] = pieces[1];
			else if(pieces[0] == "rule")
				params["rule"    ] = pieces[1];
			else if(pieces[0] == "symmetry")
				params["symmetry"] = pieces[1];
			else
				console.log("Unknown query parameter: " + queryParameters[i]);

		}
		
	}

	// at this point, we SHOULD have extract an apgcode at the very least. If
	// not, bail out.
	if(!params["apgcode"])
		return null;

	// pathologicals do not have an object code apart from the prefix
	// itself.
	if(params["apgcode"] == "PATHOLOGICAL") {

		params["prefix"] = "PATHOLOGICAL";
		params["object"] = "";

	} else {

		// separate prefix from code proper.
		// FIXME: shouldn't simply assume this succeeds, I guess.
		var pieces = codeRegex.exec(params["apgcode"]);

		params["prefix" ] = pieces[1];
		params["object" ] = pieces[2];

	}

	// other parameters go here.

	// find title heading
	var titleHeading = findTitleHeading();

	// if this object has a name (other than its apgcode), remember that.
	if(titleHeading.textContent != params["apgcode"])
		params["name"] = titleHeading.textContent;

	// return final collection of parameters.
	return params;

}
Apologies for not just copying the full current version of the script, BTW. The reason for that is simple - in addition to doing other changes I've been factoring out injected scripts into their own extension-local files. I don't think this works in Greasemonkey, though; if you want all the latest and greatest use Opera and install the extension. ;) (Or just download the Opera extension file and take a peek inside, it's basically a regular ZIP file.)

(I may try to create a Firefox version of the extension at some point, but since I'm not using Firefox for Catagolue due to its dog-slow handling of SVG animation, it's not high on my list.)

EDIT 2: I've submitted version 3.9 of the extension for moderator approval on Opera's add-on site. This contains the above fix, and also implements two further changes: apgcodes are always displayed on the page even when the object has a proper name, and comments can be collapsed and expanded. I'll go out on a limb and predict the new version will be greenlit tomorrow.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
BlinkerSpawn
Posts: 1992
Joined: November 8th, 2014, 8:48 pm
Location: Getting a snacker from R-Bee's

Re: Catagolue browser extension

Post by BlinkerSpawn » November 16th, 2016, 11:41 am

Apple Bottom wrote:EDIT 2: I've submitted version 3.9 of the extension for moderator approval on Opera's add-on site. This contains the above fix, and also implements two further changes: apgcodes are always displayed on the page even when the object has a proper name, and comments can be collapsed and expanded. I'll go out on a limb and predict the new version will be greenlit tomorrow.
Will this ever be released for other browsers too?
LifeWiki: Like Wikipedia but with more spaceships. [citation needed]

Image

drc
Posts: 1664
Joined: December 3rd, 2015, 4:11 pm

Re: Catagolue browser extension

Post by drc » November 16th, 2016, 6:01 pm

Is it possible to implement a notification for when a certain object shows up in a certain rule? and you can set them?

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » November 16th, 2016, 6:48 pm

BlinkerSpawn wrote:Will this ever be released for other browsers too?
Not for the moment, sorry. I'm lacking the time to work on it really, and support for other browsers isn't an itch of my own that I'd have to scratch.

But feel free to port it, the code's all GPL'ed (or GPL-compatible, if it's 3rd-party code I pulled in). The extension file on Opera is really just an archive that has the manifest, icons, scripts etc. in it.
drc wrote:Is it possible to implement a notification for when a certain object shows up in a certain rule? and you can set them?
Pretty much anything is possible to implement, in principle. Patches welcome!
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » January 13th, 2017, 1:52 pm

I've uploaded 4.0 to Opera Addons. The version number simply rolled around from 3.9, there's no major changes: in fact the only change is a fix for sample soup links for the new "SS" symmetry, generated by the new "hs 1.0" tool.

The extension should be greenlit by the moderators soon, but given that the weekend's coming up it might take until early next week.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » April 29th, 2017, 5:44 pm

I've pushed 4.1 to Opera Addons. This version makes three changes:
  • Sample soup links are grouped into "chunks" (currently hardcoded to contain 5 soups each).
  • Sample soup links have a title (displayed when hovering over the link) indicating which soup they point to.
  • The sample soup overlay contains a "bare" link to the soup in question.
The first two make it easier to home in on the right soup, if you're looking for a specific one. The third allows users to copy/paste sample soup links right from the sample soup overlay. (Right click, "Copy link address").

Here's what the first two changes look like (imagine a mouse cursor as well, the screenshot lost it):
hGNb1zU.png
hGNb1zU.png (12.1 KiB) Viewed 281 times
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
BlinkerSpawn
Posts: 1992
Joined: November 8th, 2014, 8:48 pm
Location: Getting a snacker from R-Bee's

Re: Catagolue browser extension

Post by BlinkerSpawn » May 1st, 2017, 10:51 am

Ok so I grabbed 4.0.1 from the Opera site (4.1 didn't appear on the download page yet), extracted the archive and pasted the .js files into Tampermonkey as separate userscripts but object search page URLs such as "https://catagolue.appspot.com/object?ap ... rule=b3s23" still don't trigger Catagolue Reloaded even after I add "*://catagolue.appspot.com/object?apgcode=*" under "User includes".
LifeWiki: Like Wikipedia but with more spaceships. [citation needed]

Image

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » May 1st, 2017, 11:30 am

BlinkerSpawn wrote:Ok so I grabbed 4.0.1 from the Opera site (4.1 didn't appear on the download page yet), extracted the archive and pasted the .js files into Tampermonkey as separate userscripts but object search page URLs such as "https://catagolue.appspot.com/object?ap ... rule=b3s23" still don't trigger Catagolue Reloaded even after I add "*://catagolue.appspot.com/object?apgcode=*" under "User includes".
Hmm. I just tried in Opera (using the extension as packaged), and it's working just fine on object search pages there.

You may have to adjust the @include line (near the top) in catagoluereloaded.js -- IIRC you can specify several, so adding a second one saying

Code: Select all

// @include     *://catagolue.appspot.com/object?*
should work. (I hope.) In Opera, it's the manifest file that determines what pages the extension runs on instead, but if you unpack it and just import the individual scripts that'll be ignored.

Is the sample soup overlay working in TamperMonkey, BTW? The extension currently injects some extra Javascript into the page to handle those. (This is seaprate from the extension code itself, which, while also written in Javascript, does not run in the page's context.) I could see this being an issue if the files for the scripts-to-inject can't be accessed.

If you or anyone else wants to pick up the torch and turn this into a proper Firefox extension, that would be excellent, BTW.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
BlinkerSpawn
Posts: 1992
Joined: November 8th, 2014, 8:48 pm
Location: Getting a snacker from R-Bee's

Re: Catagolue browser extension

Post by BlinkerSpawn » May 1st, 2017, 3:22 pm

Apple Bottom wrote:Is the sample soup overlay working in TamperMonkey, BTW?
No, nothing happens.
EDIT: Aha, do I need the scripts under the "scripts" folder too?
I had just added the standalone .js files (catagoluereloaded.js, maincensuspage.js, symmetryinjector.js) when I was having the problem but I peeked in there and that stuff looks like stuff I should've added.
LifeWiki: Like Wikipedia but with more spaceships. [citation needed]

Image

User avatar
Apple Bottom
Posts: 1034
Joined: July 27th, 2015, 2:06 pm
Contact:

Re: Catagolue browser extension

Post by Apple Bottom » May 2nd, 2017, 2:37 am

BlinkerSpawn wrote:No, nothing happens.
EDIT: Aha, do I need the scripts under the "scripts" folder too?
I had just added the standalone .js files (catagoluereloaded.js, maincensuspage.js, symmetryinjector.js) when I was having the problem but I peeked in there and that stuff looks like stuff I should've added.
Yes, those are the ones that are injected into the page.

If loading them from files isn't possible in TamperMonkey you could modify the main script to just include them as strings instead; that should work, then.
If you speak, your speech must be better than your silence would have been. — Arabian proverb

Catagolue: Apple Bottom • Life Wiki: Apple Bottom • Twitter: @_AppleBottom_

Proud member of the Pattern Raiders!

User avatar
BlinkerSpawn
Posts: 1992
Joined: November 8th, 2014, 8:48 pm
Location: Getting a snacker from R-Bee's

Re: Catagolue browser extension

Post by BlinkerSpawn » May 2nd, 2017, 8:17 am

Apple Bottom wrote:If loading them from files isn't possible in TamperMonkey you could modify the main script to just include them as strings instead; that should work, then.
How would I go about doing this?
LifeWiki: Like Wikipedia but with more spaceships. [citation needed]

Image

Post Reply