User:Yair rand/SWKB.js

From Wikimedia Incubator
Jump to navigation Jump to search

Note: After saving, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Internet Explorer: Hold Ctrl while clicking Refresh, or press Ctrl-F5
  • Opera: Go to Menu → Settings (Opera → Preferences on a Mac) and then to Privacy & security → Clear browsing data → Cached images and files.
// SignWriting Keyboard script by Yair Rand ([[User:Yair rand]])
// (I'm calling it just "SignWriting Keyboard script" for now. Actual name pending.)
// Licensed under GPL v2+, CC-BY-SA 3.0 or higher, and GFDL.

window.SWKB = ( window.SWKB && SWKB.loaded === true ) ? SWKB : ( function () {
	
	/**
	 * ** SOME GENERAL DOCUMENTATION **
	 * 
	 * The SignWriting Keyboard (or, as it seems to be called these days, the 
	 * Rand SignWriting Keyboard) is a tool for typing SignWriting text onto 
	 * ordinary text boxes (input and textarea elements) on web pages.
	 * 
	 * The SWKB is basically divided into three major parts: the keyboard 
	 * object, the layout, and the "content" objects which are SignAreas,
	 * Signs, SignSymbols, and NonSignTexts.
	 * 
	 * The keyboard object handles, well, the keyboard: the big element at the 
	 * bottom of the screen. It also handles incoming key events and such.
	 * 
	 * The layout holds the general "mapping" of keys (layout.baseKeyboardLayout,
	 * and later layout.modeMap) to functions (in layout.actions) which change
	 * properties of SignSymbols. After each keypress (and whenever else the layout
	 * calls it), SignSymbol.updateImage is called, which calls layout.symbolFSW 
	 * which interprets the various properties and returns what the FSW (Formal 
	 * SignWriting) of the symbol (not including positioning) should be. The 
	 * changeLayout function can be used to switch layouts.
	 * 
	 * There is currently only one functioning layout generator, BaseLayout.
	 * At some point, there might also be DOSLayout, based off the 
	 * SignWriter-DOS program's layout, if anyone wants to try to create it. I
	 * created a basic skeleton layout at DOSLayout, right below BaseLayout.
	 * This can be enabled by adding ?use-dos to the url when running the 
	 * script, or by selecting "DOS" in the layout section of the settings menu. 
	 * 
	 * SignSymbols hold whatever internal properties might be used by the layout,
	 * along with the X and Y positioning properties, and the symbolText and 
	 * signText properties. Signs hold symbols in the "symbols" property, and
	 * SignAreas hold Signs in the "signs" property. (SignAreas also have 
	 * "history" properties which record things for purposes of undo/redo.)
	 * The "active" signarea and sign are the signArea and activeSign variables,
	 * respectively. SignSymbols are a little more complicated; activeSymbols is
	 * an object, which can hold two active symbols in case you have a split 
	 * righthand/lefthand layout. activeSymbols[ false ] is the leftHand active 
	 * SignSymbol, or the only active SignSymbol if the layout is one-handed. 
	 * (Each of these constructors have their own prototypes filled with many 
	 * important functions that I'm not going to list up here.)
	 * 
	 * So, when the user presses a key:
	 * (Event) > keyboard.setKeyState > (...) > 
	 * 		> SignArea.history.pushState() // record state before changes run.
	 * 		> layout.baseKeyboardActions[ layout.modeMap( symbol )[ position of pressed key ] ]
	 * 			.call( symbol, rightHand, keyboard position, map "icon", shiftKey )
	 * 		> SignSymbol.updateImage()
	 * 			> layout.symbolFSW.call( symbol ) // return FSW
	 * 			> SignSymbol.updateSignText() // Set FSW, based on symbolFSW
	 * 			> set src of the SignSymbol's element, based on above return value.
	 * 		> keyboard.update() // Update the visible keyboard.
	 * 			> (Call a layout.baseKeyboardActions on every key, passing a 
	 * 			   fake symbol with fake=true, in order to populate the key 
	 * 			   elements with images.)
	 * 
	 * Other things of note are the languageData, i18n, and keyMaps objects. If
	 * you are able to translate any of the key labels to a sign language, 
	 * please take a look at i18n. If you want to add a fingerspelling layout 
	 * for a sign language, please look at languageData. If you want to add a 
	 * different keyboard layout (in the QWERTY, Dvorak, QWERTZ, AZERTY sense of
	 * layout, not like DOS or BaseLayout) please look at keyMaps.
	 * 
	 * Now, I also intend to have this script generally usable all over the web,
	 * so that means that this needs to be able to deal with textboxes that are
	 * doing their own thing, as well as content that isn't Signs. A lot of
	 * extra stuff to deal with these things is turned off when 
	 * config.ce === false. When it's on:
	 * * We have the SignArea's element have contenteditable=true, and 
	 *   continually do things with the caret. To that end, there's a specific
	 *   "caret" object filled with properties and functions and whatnot.
	 * * There are "NonSignText"s, equivalent to Signs but holding simple 
	 *   plaintext, instead of Signs. 
	 * * delegateEvent (to be renamed) is continually passing all the events 
	 *   to the original plain textbox, so that other javascript can react to
	 *   things as normal. 
	 * * We're also continually synchronizing the content and caret positions 
	 *   of the textbox and SignArea, in both directions. Yes, this is 
	 *   expensive, and yes, it's absurdly complicated. Sorry.
	 */
	
	// Keeping this here for testing purposes only. When in use, this gets split
	// off into a separate CSS file.
	window.mw && mw.util.addCSS( "\
	@font-face { \
		font-family: 'SignWriting 2010'; \
		/* src: url( 'SignWriting 2010.ttf' ) format( 'truetype' ); */ \
		/* src: url( 'https://googledrive.com/host/0B1Pz4n6wXAfvT18yOTFoSE5Sa2s/SignWriting 2010.ttf' ) format( 'truetype' ); */ \
		src: url( 'https://cdn.rawgit.com/Slevinski/signwriting_2010_fonts/master/fonts/SignWriting%202010.ttf' ); \
	} \
	@font-face { \
		font-family: 'SignWriting 2010 Filling'; \
		/* src: url( 'SignWriting 2010 Filling.ttf' ) format( 'truetype' ); */ \
		/* src: url( 'https://googledrive.com/host/0B1Pz4n6wXAfvT18yOTFoSE5Sa2s/SignWriting 2010 Filling.ttf' ) format( 'truetype' ); */ \
		src: url( 'https://cdn.rawgit.com/Slevinski/signwriting_2010_fonts/master/fonts/SignWriting%202010%20Filling.ttf' ) format( 'truetype' ); \
	} \
	.SWkeyboard { \
		position: fixed; \
		/* right: 0; */ \
		/* left: 0; */ \
		bottom: 0; \
		background-color: #FFFFFF; \
		box-shadow: 0 0 1px rgba(0,0,0,0.3); \
		z-index: 105; \
		-webkit-transition: bottom 0.5s; \
		transition: bottom 0.5s; \
		writing-mode: lr-tb; \
		writing-mode: horizontal-tb; \
		-ms-writing-mode: lr-tb; \
		-webkit-writing-mode: horizontal-tb; \
		-moz-writing-mode: horizontal-tb; \
	} \
	.SWkeyboard div { \
		display: inline-block; \
		position: relative; \
		height: 50px; \
		width: 50px; \
		margin: 1px; \
		border-radius: 2px; \
		border: 1px solid #AAA; /* Consider changing to #BBB */ \
		background-position: 50% 50%; \
		background-repeat: no-repeat; \
		background-color: #FFFFFF; \
		text-align: center; \
		float: left; \
		word-wrap: break-word; \
		overflow: hidden; \
		font-size: 10px; \
		cursor: pointer; \
		box-shadow: rgba(0,0,0,.15) 0px 0px 5px inset; \
		-webkit-transition: width .15s, margin-right .15s, background-position 0s; \
		-moz-transition: width .15s, margin-right .15s, background-position 0s; \
		-o-transition: width .15s, margin-right .15s, background-position 0s; \
		transition: width .15s, margin-right .15s, background-position 0s; \
	} \
	.SWkeyboard div.pressed { \
		box-shadow: rgba(0,0,0,.25) 0px 0px 10px inset; \
	} \
	.SWkeyboard div:hover:not(.pressed) { \
		box-shadow: rgba(0,0,0,.5) 0px 0px 1px inset; \
	} \
	.SWkeyboard br { \
		clear: both; \
	} \
	.SWkeyboard .SWKB-settings { \
		margin: 15px; \
		height: 20px; \
		background-image: \
			url(//upload.wikimedia.org/wikipedia/commons/thumb/0/05/OOjs_UI_icon_advanced.svg/15px-OOjs_UI_icon_advanced.svg.png ); \
		background-position: center 2px; \
		overflow: visible; \
	} \
	.SWkeyboard .SWKB-moveicon { \
		margin: 5px; \
		height: 30px; \
		width: 30px; \
		float: right; \
		background-image: \
			url(//upload.wikimedia.org/wikipedia/commons/thumb/c/ca/OOjs_UI_icon_move.svg/20px-OOjs_UI_icon_move.svg.png ); \
		background-position: center center; \
		border: 0; \
		box-shadow: none; \
		cursor: move; \
	} \
	.SWkeyboard .SWKB-settings-menu { \
		display: none; \
		position: absolute; \
		bottom: 100%; \
		left: 15%; \
		box-shadow: 1px 1px 0px rgba(0,0,0,.3); \
		border-radius: 0; \
		height: auto; \
		width: 90px; \
		padding: 5px 8px; \
		font-size: 11px; \
		text-align: left; \
	} \
	.SWkeyboard .SWKB-settings-menu label { \
		display: block; \
	} \
	.SWkeyboard .SWKB-settings-menu label:hover { \
		background-color: #E3F6FF; \
	} \
	.SWKB-KBLetter { \
		font-size: 13px; \
		color: #AAA; \
		position: absolute; \
		left: 4px; \
		bottom: 4px; \
	} \
	.SWKB-SignArea { \
		position: absolute; \
		/* overflow: auto; */ \
		z-index: 10; \
		/* background-color: rgba( 230, 240, 255, 0.6 ); */ \
		background-color: #FFFFFF; \
		/* opacity: 0.5; */ \
		font-size: 30px; \
	} \
	.SWKB-SignArea > div { \
		background-color: #FFFFFF; \
		/* outline: none; */ \
		writing-mode: tb-lr; \
		writing-mode: vertical-lr; \
		-moz-writing-mode: vertical-lr; \
		-webkit-writing-mode: vertical-lr; \
		/* min-width: 100%; */ \
	} \
	.SWKB-Sign { \
		position: relative; \
		width: 500px; \
		height: 500px; \
		border-left: 1px solid #DDDDDD; \
		background-color: #FFFFFF; \
		margin: 14px 1px; \
		font-size: 30px; \
		overflow: hidden; \
		display: inline-block; \
		vertical-align: middle; \
	} \
	.SWKB-Sign div { \
		width: 100px; \
		height: 100px; \
		position: absolute; \
	} \
	.SWKB-Sign img { \
		position: absolute; \
		z-index: 2; \
		-webkit-user-drag: none; \
		-webkit-transform-origin: 0 0; \
		-ms-transform-origin: 0 0; \
		-moz-transform-origin: 0 0; \
		-o-transform-origin: 0 0; \
		transform-origin: 0 0; \
	} \
	.SWKB-activeSign img:hover:not(.activeSymbol):not(.SWKB-PS) { \
		outline: #1066DD 1px dashed; \
		/* TODO: Change to text-shadow after webfont is ready. */ \
	} \
	.SWKB-Sign img.activeSymbol { \
		/* opacity: 0.65; */ \
		filter: alpha(opacity=65); \
		-webkit-animation: SWKBaS 0.4s; \
		animation: SWKBaS 0.4s; \
		/* This should probably also use a color and/or text-shadow once possible. */ \
	} \
	.SWKB-Sign span.activeSymbol { \
		text-shadow: 1px 0.5px 2px #0077FF; \
		/* Add a color. Also, consider using different colors for right */ \
		/* hand and left hand. Orange/f93 and light blue/07f, maybe. Also */ \
		/* have colors visible on the sides of the keyboard. */ \
	} \
	.SWKB-Sign img.activeSymbol-L { \
		/* opacity: 0.65; */ \
	} \
	.SWKB-Sign img.activeSymbol-R { \
		/* opacity: 0.65; */ \
	} \
	.SWKB-Sign [data-swkb-symbol] { \
		position: absolute; \
		/* font-size: 30px; */ \
		/* line-height: 100%; */ \
		line-height: 30px; \
		writing-mode: lr-tb; \
		writing-mode: horizontal-tb; \
		-ms-writing-mode: horizontal-tb; \
		-moz-writing-mode: horizontal-tb; \
		-webkit-writing-mode: horizontal-tb; \
	} \
	.SWKB-Sign [data-swkb-symbol]:before { \
	/* .SWKB-Symbol */ \
		position: absolute; \
		font-family: 'SignWriting 2010'; \
		content: attr( data-SWKB-symbol ); \
	} \
	.SWKB-Sign [data-swkb-symbol]:after { \
		position: absolute; \
		font-family: 'SignWriting 2010 Filling'; \
		content: attr( data-SWKB-symbol ); \
		color: #FFFFFF; \
		font-weight: normal; \
	} \
	.SWKB-Sign [data-swkb-symbol]:hover { \
		color: #006666; \
		font-weight: bold; \
	} \
	.SWKB-SignArea .SWKB-Sign span.activeSymbol-L { \
		color: #0000FF; \
		font-weight: normal; \
	} \
	.SWKB-SignArea .SWKB-Sign span.activeSymbol-R { \
		color: #00CC00; \
		font-weight: normal; \
	} \
	.SWkeyboard br + div { \
		margin-left: 70px; \
	} \
	.SWkeyboard br ~ br + div { \
		margin-left: 90px; \
	} \
	.SWkeyboard br ~ br ~ br + div { \
		margin-left: 110px; \
	} \
	.SWKB-KBSymbol { \
		position: absolute; \
		font-size: 30px; \
		line-height: 30px; \
		margin-top: -15px; \
	} \
	.SWKB-KBSymbol:before { \
		position: absolute; \
		font-family: 'SignWriting 2010'; \
		content: attr( data-sw-symbol ); \
		color: #000000; \
	} \
	.SWKB-KBSymbol:after { \
		position: absolute; \
		font-family: 'SignWriting 2010 Filling'; \
		content: attr( data-sw-symbol ); \
		color: #FFFFFF; \
	} \
	span.SWKB-KBicon { \
		position: absolute; \
		left: 0; \
		top: 0; \
		height: 100%; \
		width: 100%; \
	} \
	@-webkit-keyframes SWKBaS { \
		0% {opacity: 0.65;} \
		25% {opacity: 0.5;} \
		100% {opacity: 0.65;} \
	} \
	@keyframes SWKBaS { \
		0% {opacity: 0.65;} \
		25% {opacity: 0.5;} \
		100% {opacity: 0.65;} \
	} \
	@-webkit-keyframes SWKBblink { \
		50% {background-color:#AAA;} \
		100% { background-color: rgba( 0, 0, 0, 0.07 ); } \
	} \
	@keyframes SWKBblink { \
		50% { background-color: rgba( 0, 0, 0, 0.07 ); } \
		100% {background-color:#AAA;} \
	} \
	.SWKB-PS { \
		display: none; \
	} \
	.SWKB-activeSign .SWKB-PS { \
		display: block; \
		z-index: 1; \
	} \
	.SWKB-Sign div.SWKB-crosshairX { \
		width: 100%; \
		height: 2px; \
		left: 0; \
		top: 249px; \
		/* background-color: #CCCCCC; */ \
		/* background-color: rgba( 0, 0, 0, 0.07 ); */ \
	} \
	.SWKB-Sign div.SWKB-crosshairY { \
		height: 100%; \
		width: 2px; \
		top: 0; \
		left: 50%; \
		margin-left: -1px; \
	} \
	.SWKB-Sign.SWKB-activeSign div.SWKB-crosshairY, \
	.SWKB-Sign.SWKB-activeSign div.SWKB-crosshairX { \
		background-color: #AAA; \
		-webkit-animation: SWKBblink 1060ms step-start infinite; \
		animation: SWKBblink 1060ms step-start infinite; \
		/* background-color: #BBBBBB; */ \
		/* background-color: rgba( 0, 0, 0, 0.2 ); */ \
	} \
	.SWKB-Sign:hover:not(.SWKB-activeSign) { \
		outline: #1066DD 1px dashed; \
		outline-offset: 3px; \
	} \
	.SWKB-Sign .SWKB-FSW { \
		font-size: 0; \
		position: absolute; \
		height: 0; \
	} \
	.SWKB-Sign-ce { \
		width: 100%; \
		height: 100%; \
		outline: none; \
		display: block; \
		opacity: 0; \
		color: transparent; \
		overflow: hidden; \
	} \
	.SWKB-copyPasteBox { \
		opacity: 0; \
		position: fixed; \
		top: 0; \
	} \
	.SWKB-NonSign { \
		min-height: 1em; \
		width: 100%; \
		font-family: monospace; \
		outline: none; \
		outline: 0px solid transparent; \
		/* preserve whitespace */ \
		white-space: pre-wrap; \
		-webkit-user-modify: read-write-plaintext-only; \
		vertical-align: middle; \
		zoom: 1; \
	} \
	.SWKB-NonSign:empty { \
		display: inline-block; \
		min-width: 1em; \
	} \
	.SWKB-NonSign .SWKB-caretHolder { \
		display: inline-block; \
		min-height: 1px; \
		width: 100%; \
		min-width: 1em; \
	} \
	.SWKB-NonSign .SWKB-caretHolder br { \
	} \
	@media print { \
		.SWKB-Sign:hover:not(.SWKB-activeSign) { \
			outline: none; \
		} \
	} \
	@media screen and (max-width: 790px) { \
		.SWkeyboard div { \
			height: 25px; \
			width: 30px; \
			margin: 1px 0 0 1px; \
			font-size: 7px; \
			overflow: visible; \
		} \
		.SWkeyboard br + div { \
			margin-left: 45px; \
		} \
		.SWkeyboard br ~ br + div { \
			margin-left: 55px; \
		} \
		.SWkeyboard br ~ br ~ br + div { \
			margin-left: 65px; \
		} \
		.SWkeyboard .SWKB-moveicon { \
			margin: 1px; \
			height: 25px; \
		} \
		.SWkeyboard .SWKB-settings { \
			margin: 3px 5px 3px 15px; \
		} \
		.SWKB-KBLetter { \
			font-size: 8px; \
			left: 2px; \
			bottom: 2px; \
		} \
		.SWKB-KBSymbol { \
			font-size: 15px; \
			/* margin-top: -25px; */ \
			/* margin-left: -9px; */ \
		} \
	} \
	@media screen and (max-width: 620px) /* 360 */ { \
		.SWkeyboard div { \
			width: 20px; \
		} \
		.SWkeyboard br + div { \
			margin-left: 35px; \
		} \
		.SWkeyboard br ~ br + div { \
			margin-left: 45px; \
		} \
		.SWkeyboard br ~ br ~ br + div { \
			margin-left: 55px; \
		} \
		.SWkeyboard .SWKB-moveicon { \
			width: 25px; \
		} \
	} \
	/* temporary wikipedia extras */ \
	.suggestions { \
		display: none !important; \
	} \
	" );
	
	/*
	(function(){
		var x = document.getElementsByTagName( "head" )[ 0 ].appendChild( 
			document.createElement("link") );
		x.type = "text/css";
		x.rel = "stylesheet";
		x.href = "https://806e8abcad89c3043886d9f62a7903edbba39fa1.googledrive.com/host/0B1Pz4n6wXAfvT18yOTFoSE5Sa2s/SWKB2.css";
	})();
	*/
	
	/*
	x.appendChild( document.createTextNode( 
		"@font-face{font-family:'SignWriting 2010';"+
		"src:url('SignWriting 2010.ttf') format('truetype');}"+
		"body{background-color:blue;font-family:'SignWriting 2010';}" ));
	document.body.lastElementChild.innerText='􆜡'
	*/
	
	/*
	
	This is all vanilla javascript, and I'd like to keep it that way.
	
		Current layout:
		
	?  D  L  R  ↑  ?  \  /  ?  ↑  L  R  D  ---
	    P  ←  ↓  →  O  |  O  ←  ↓  →  P  F  ? 
	     1  2  3  4  N  N  4  3  2  1  ?      
	----  B  ☺  #  5  !  5  #  ☺  B  ?  ------
		      |   Next Sign   |               
	
	Considering:	
	?  ?  L  R  ↑  N  \  /  N  ↑  L  R  ?  ---
	    P  ←  ↓  →  O  |  O  ←  ↓  →  P  F  ? 
	     1  2  3  4  5  5  4  3  2  1  ?      
	----  B  ☺  #  D  !  D  #  ☺  B  ?  ------
		      |   Next Sign   |                
	
	Or maybe:
	?  ?  #  B  ↑  D  \  /  D  ↑  B  #  ?  ---
	    ☺  ←  ↓  →  P  |  P  ←  ↓  →  ☺  F  ? 
	     1  2  3  4  O  O  4  3  2  1  ?      
	----  N  L  R  5  !  5  L  R  N  ?  ------
		      |   Next Sign   |               
	
	Punctuation map uses home row +three bottom middle keys.
	
	Immediate todo list:
	* Sync textbox even when ce: false
	* If possible, finish the menu.
	** Also, rewrite a bit of settings so that we know what things we're on.
	** Make it a keyboard prop.
	* Look into text toggle code
	* IE TODOs: 
	** Still randomly clips off random strings. Weird.
	*** This is caused by IE handling innerText and/or pseudo-normalize weirdly.
	**** Added workaround for innerText. Should be fixed.
	** Hide keyboard immediately.
	** Focusing the tb makes things go crazy. See SWKBify. 
	*** The problem appears to be in handleTextboxChange.
	** Two presses in quick succession breaks things. Probably something to do
	   with one of the setTimeouts. Maybe in SWKBify? TODO: Look into it.
	** Box is mispositioned to the right in some cases. See searchbox.
	** Need to get caret.keepInView working.
	** Find workaround for right/left arrow keys.
	* FF TODOs:
	** Still some minor issues with caret movement. Also, blurring doesn't work.
	** Backspacing empty NSTs doesn't work.
	* Set up config options for not requiring focus before building SW boxes.
	* Add settings system for font size changes.
	* Heights for small symbols is still full line-height. Messes up mouse 
	  controls. TODO: Set height based on heightCache.
	
	Notes:
	* Consider: Moving 5 > N, N > ?, D > 5.
	* Find some use for shift over the finger keys.
	** Currently, shift speeds up movement and rotate keys, flips face, adds 
	   duplicate and swap options to next and middlerow.
	* There are 261 handshapes, and only 32 finger combinations.
	* Consider: Moving the three lanes to the right side of the keyboard?
	* >61% of symbols (on Wp/ase/AS119...) are handshapes
	** Can punct mark become merge mark? Could have weird (multihand?) controls.
	* TODO: Incubator integration, Size fixes...
	** Size fixes needs to wait until either SWIS or SWAP support sizes in SVGs.
	*** Or until truetype is ready.
	* TODO: Consider adding the space bar to the onscreen keyboard.
	* TODO: Have the signs shrink horizontally when space is wasted.
	** Not sure this is possible.
	* TODO: Have a way to change the zIndex of symbols.
	* TODO: Move everything specific to my keyboard layout to its own section.
	** Mostly done.
	** Note: There's not going to be a good way to switch modes mid-symbol.
	   It's probably going to require parsing to FSW and then re-interpreting.
	* TODO: Prepare things to work with either truetype font or images.
	* Possible bug: Punctuation signs have different dimensions then those on
	  the wiki pages. TODO: Investigate.
	* TODO: Selection handling. 
	* TODO: Keep the selected Sign in view, keep the right scroll position.
	** One way to do this might be to just let the keys fall through, then
	   undo their effects afterward.
	*** Nope, that doesn't keep the whole thing in view.
	* TODO: Sane error handling so that if anything gets blown up by a clumsy
	  user (we're using contenteditable, remember), we don't lose everything.
	* TODO: Figure out how to make the keyboard not cover the signs we're trying
	  to edit. Or anything else important, preferably.
	** Currently using a drag-drop system. Not sure if that's sufficient.
	* TODO: Add a method of disabling the keyboard, with a method to re-enable.
	** If re-enabling is done by keyboard, could be problematic. Still probably
	   the best way, though...
	* TODO: Fill in documentation on extraKeys and actionGenerators.
	* TODO: Ctrl-X system.
	* TODO: Find out what's up with the forgetting-to-notice-that-we-changed-NSTs
	  in syncTB.
	* TODO: Fix the odd paste-position bug.
	* TODO: Fix copy bug in IE8. 
	* The Pageup/pagedown bug in Chrome is a Chrome bug, not a script bug.
	* TODO: Investigate iframes. Key events do not automatically flow to outside
	  them. Useful for dealing with goog's always-sieze-focus. Maybe also check 
	  shadow dom.
	* TODO: Fix paste issue with pasting plaintext onto other plaintext.
	* TODO: Build config.scaleTo. Maybe default it to 1, at least for a while. (Signs 
	  change size with setDomSize.)
	* BUG: First symbol created doesn't fire a textbox update.
	** Argh, that's caused by lack of height values. Can't fix it unless I attach
	   things to onload. Glurk.
	*** So, needs another textbox update each time. This is going to be hard on
	    performance, unless I get NST to cache stuff first.
	*** Done, sort of.
	** Actually, I'm starting to think there's no real way to perfectly fix this.
	   The real signText isn't available until after the load is done, and then
	   the keystroke business is finished, so things won't respond. Using the
	   previous dimensions will make it seem to be "lagging" one keystroke behind
	   with regards to sizing of the box. Serious problem...
	*** Maybe not so serious. First key doesn't do anything, but later ones do,
	    and the box gets updated before submitting anyway.
	* TODO: Find out what's up with the add-link boxes.
	* TODO: Handle placeholder attribute.
	* TODO: Maybe split tutorial, or start globalizing it.
	* TODO: Improve keepInView performance. Try to prevent extra range add-removes.
	* TODO: Home key, end key, left and right keys for Signs.
	** Option 1: Add a post-edit hook, and append caret.check to that.
	*** Didn't work.
	* TODO: Ctrl-arrow keys for NSTs.
	* TODO: Figure and why clicking on a selection isn't working, and fix it.
	* Um, are we not regularly updating the text box when editing Signs?
	** Only when pressing keys using the mouse. TODO: Fix, I guess.
	* TODO: Body-Finger symbols.
	* TODO: Rename .fake, to make keyboard simulating clearer.
	* TODO: Consider replacing signArea/activeSign/activeSymbols with single
	  "active" object. 
	* TODO: Deal with switching to/from NSTs. General, both texts and backgrounds. (Keyboard issues.)
	
	* Did a quick performance test per keypress: 
	** plain images: ~3ms, fontimages: ~40ms, fonts: ~30ms
	* TODO: Try to speed up the canvas mess. Maybe cache things, other stuff.
	*/
	
	var config = simpleExtend( ( window.SWKB && SWKB.config ),  {
			ce : true, // ContentEditable
			autostart: false, // Set up a simple SignArea in the top-left corner
			                 // of the screen, not associated with any text box.
			copyAll: false,   // Whether Ctrl-C copies everything or only the 
			                 // current selection.
			useFont: false, // Use fonts directly. (Not canvassing or SWIS.)
			noFont: true, // Suppress all font usage, incl. canvas.
			onlyOnFocus: true, 
			enableDOSMode: location.search.indexOf( "use-dos" ) !== -1,
			defaultKeyMap: "qwerty",
			defaultAlignLeft: false,
			useLang: false, // Override default language guess.
			signWidth:  300, 
			signHeight: 500,
			signAreaHeight: 200, // baseHeight
			scaleSigns: false,
			scaleBy: 1, // Not yet implemented
			numberOfSuggestions: 5
		} ),
		// TODO: Internationalization. Need to ask on the list for translations
		// of these into sign languages.
		i18n = {
			// Default to mostly ase for now. Still need to set up proper i18n.
			// Leave en here for the translators.
			en: {
				leftlane: "Left lane",
				rightlane: "Right lane",
				middlelane: "Middle lane",
				nextsymbol: "Next symbol",
				deleteleftsymbol: "Delete lefthand symbol",
				deleterightsymbol: "Delete righthand symbol",
				duplicate: "Duplicate symbol",
				fingerspell: "Fingerspelling mode",
				autocomplete: "Autocomplete",
				swapsides: "Swap sides"
			},
			ase: {
				nextsymbolleft : "M520x522S2b711485x479S15a02493x505S15a0a480x510",
				nextsymbolright : "M521x521S15a0a480x502S15a02494x509S2b700498x480",
				fingerspell : "M524x522S14c50481x491S22520476x478S26506509x502",
				deleteleftsymbol : "M510x522S1ea4f491x494S20e00495x478",
				deleterightsymbol : "M510x522S1ea47491x494S20e00494x478",
				duplicate: "M527x514S15a18473x487S18510502x495S26506487x495S22104509x486"
			}
		},
		signAreas = [], // Full list of all SignAreas.
		signArea, // Active SignArea, assuming there is one.
		signListElem = document.createElement( "div" ), // why is this here?
		activeSign,
		activeSymbols = {},
		fakeSymbol,
		layout,
		keyboard, 
		prefs,
		// These'll get removed after the webfont is ready.
		widthCache = {}, heightCache = {},
		// These variables should probably be moved somewhere else.
		keyMaps = {
			qwerty : {
				label : "QWERTY",
				layout : 
					( "192,49,50,51,52,53,54,55,56,57,48,189,187," + 
					       "81,87,69,82,84,89,85,73,79,80,219,221,220," + 
					        "65,83,68,70,71,72,74,75,76,186,222," + 
					         "90,88,67,86,66,78,77,188,190,191" ).split(",")
			},
			dvorak : {
				label : "Dvorak",
				layout :
					( "192,49,50,51,52,53,54,55,56,57,48,219,221," + 
					       "222,188,190,80,89,70,71,67,82,76,191,187,220," +
					        "65,79,69,85,73,68,72,84,78,83,189," + 
					         "186,81,74,75,88,66,77,87,86,90" ).split(",")
			},
			// NOTE: 192 overlaps with `, the togglePlaintext extra. Breakage.
			qwertz : {
				label : "QWERTZ",
			    layout :
				    ( "220,49,50,51,52,53,54,55,56,57,48,219,221," + 
				           "81,87,69,82,84,90,85,73,79,80,186,187,191,"+
				            "65,83,68,70,71,72,74,75,76,192_,222,"+
				             "89,88,67,86,66,78,77,188,190,189").split(","),
				letterMap: {
					// Certain letters seem to be broken. TODO: Fix.
					76: "L", 186: "Ü", 187: "+", 188: ";", 190: ":", 
					191: "#", "192_": "Ö", 219: "ß", 221: "´", 222: "Ä"
				},
				remapper: {
					220: 192,
					192: "192_"
				}
			},
			azerty : {
				label : "AZERTY",
			    layout :
				    ( "222,49,50,51,52,53,54,55,56,57,48,219,187," + 
				           "65,90,69,82,84,89,85,73,79,80,221,186,220,"+
				            "81,83,68,70,71,72,74,75,76,77,192_," +
				             "87,88,67,86,66,78,188,190,191,223" ).split(","),
				letterMap: {
					// Certain letters seem to be broken. TODO: Fix.
					221: "^", 186: "$", 220: "*", "192_": "Ù", 223: "!"
					//76: "L", 186: "Ü", 187: "+", 188: ";", 190: ":", 
					//191: "#", "192_": "Ö", 219: "ß", 221: "´", 222: "Ä"
				},
				remapper: {
					222: 192,
					192: "192_"
				}
			}
		},
		keyCodeTable = {
			48: "0", 49: "1", 50: "2", 51: "3", 52: "4", 53: "5", 54: "6", 
			55: "7", 56: "8", 57: "9", 65: "A", 66: "B", 67: "C", 68: "D", 
			69: "E", 70: "F", 71: "G", 72: "H", 73: "I", 74: "J", 75: "K", 
			76: "L", 77: "M", 78: "N", 79: "O", 80: "P", 81: "Q", 82: "R", 
			83: "S", 84: "T", 85: "U", 86: "V", 87: "W", 88: "X", 89: "Y", 
			90: "Z", 186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/",
			192: "`", 219: "[", 220: "\\", 221: "]", 222: "'"
		},
		// So, at the start, we only know the keyCode when a key is pressed.
		// If it's code to letter, each SL comes with a code-to-letter override
		// list, I guess? For display purposes, and fingerspell map purposes.
		// Okay, problem: Overrides really are keyboard-dependent, not language.
		// Except when they are language-dependent, like Hebrew and Arabic. Argh.
		// In fingerspelling the letters really are language-dependent. Partly. Sort of.
		// Ummm...
		languageData = {
			// Maybe this should be moved somewhere else. Not sure.
			"ase" : { // American Sign Language
				label: "ASL",
				code: "ase",
				letters: { // Should this have numbers and stuff?
					"A" : "M507x507S1f720487x492",
					"B" : "M507x507S14720493x485",
					"C" : "M508x507S16d20491x487",
					"D" : "M508x507S10120492x477",
					"E" : "M507x507S14a20492x492",
					"F" : "M513x507S1ce20491x477",
					"G" : "M507x507S1f000478x492",
					"H" : "M507x507S11502477x492",
					"I" : "M513x507S19220492x488",
					"J" : "M513x507S19220492x488S2a20c476x472",
					"K" : "M507x507S14020478x477",
					"L" : "M508x507S1dc20484x477",
					"M" : "M507x507S18d20487x482",
					"N" : "M507x507S11920486x481",
					"O" : "M508x507S17620492x491",
					"P" : "M510x509S14051479x485",
					"Q" : "M511x509S1f051481x486",
					"R" : "M507x507S11a20492x477",
					"S" : "M507x507S20320492x492",
					"T" : "M508x507S1fb20492x488",
					"U" : "M507x507S11520492x477",
					"V" : "M508x507S10e20493x477",
					"W" : "M509x515S18620490x485",
					"X" : "M508x507S10620487x481",
					"Y" : "M514x507S19a20486x487",
					"Z" : "M528x507S10020492x477S2450a497x473",
					"1" : "M508x515S10020493x485",
					"2" : "M508x515S10e20493x485",
					"3" : "M508x515S11e20485x485",
					"4" : "M515x516S14420493x485",
					"5" : "M512x516S14c20489x485",
					"6" : "M509x515S18720491x486",
					"7" : "M512x514S1a520491x486",
					"8" : "M513x514S1bb20492x486",
					"9" : "M513x515S1ce20491x485",
					"0" : "M508x507S17620492x491"	
				},
				shiftLetters: {
					"0" : "M508x512S1f540493x488",
					"1" : "M508x515S10000493x485",
					"2" : "M508x515S10e00493x485",
					"3" : "M516x515S11e00493x485",
					"4" : "M507x516S14400485x485",
					"5" : "M512x516S14c00489x485"
				},
				keyMap: "qwerty",
				localeGuesses: [ "en", "en-US", "en-CA" ]
			},
			"gsg" : { // German Sign Language
				label: "DGS",
				code: "gsg",
				map: // deprecated
					( "`  1  2  3  4  5  6  7  8  9  0  ß  =     " +
					  "    Q  W  E  R  T  Z  U  I  O  P  Ü  *  ' " +
					  "     A  S  D  F  G  H  J  K  L  Ö  Ä      " +
					  "      Y  X  C  V  B  N  M  ;  :  -        " ).split(/\s+/g),
				letters: { // Should this have numbers and stuff?
					"Ä" : "M517x507S23108491x472S1f720488x492",
					"Ö" : "M515x507S17610492x491S23108489x469",
					"Ü" : "M515x507S11520493x477S23108489x456",
					"SS" : "M526x508S20320492x492S26506511x494",
					"ß" : "M513x507S20320492x492S23108487x470",
					"SCH" : "M512x507S14c20489x476",
					"A" : "M508x507S1f720488x492",
					"B" : "M507x507S14720493x485",
					"C" : "M508x507S16d10491x487",
					"D" : "M508x507S10120492x477",
					"E" : "M508x507S14a20493x492",
					"F" : "M514x507S1ce20492x477",
					"G" : "M507x507S10002477x492",
					"H" : "M507x507S11502477x492",
					"I" : "M513x507S19220492x488",
					"J" : "M513x507S19220492x488S2a20c472x468",
					"K" : "M507x507S14020478x477",
					"L" : "M507x507S1dc20483x477",
					"M" : "M507x507S18d50487x482",
					"N" : "M507x507S11950485x481",
					"O" : "M508x508S17610492x492",
					"P" : "M507x507S14050478x477",
					"Q" : "M509x507S1f057486x477",
					"R" : "M507x507S11a20492x477",
					"S" : "M507x507S20320492x492",
					"T" : "M506x507S1f240476x489",
					"U" : "M507x507S11520492x477",
					"V" : "M507x507S10e20492x477",
					"W" : "M509x507S18720491x478",
					"X" : "M507x507S10610492x481",
					"Y" : "M513x507S19a20485x487",
					"Z" : "M531x507S10020492x477S2450a500x468"
				},
				keyMap: "qwertz",
				localeGuesses: [ "de", "de-DE" ]
			},
			"fcs" : { // Quebec Sign Language
				label: "LSQ",
				code: "fcs",
				letters: {
					"A" : "M507x507S1f720487x492",
					"B" : "M507x507S14720493x485",
					"C" : "M508x507S16d20491x487",
					"D" : "M508x507S10120492x477",
					"E" : "M507x507S14a20492x492",
					"F" : "M513x507S1ce20491x477",
					"G" : "M515x508S1f040486x493",
					"H" : "M507x507S11502477x492",
					"I" : "M513x507S19220492x488",
					"J" : "M513x526S19220492x475S2a20c487x498",
					"K" : "M515x515S14010486x485",
					"L" : "M508x507S1dc20484x477",
					"M" : "M512x508S14b50489x493",
					"N" : "M507x507S11950485x481",
					"O" : "M508x508S17610492x492",
					"P" : "M507x507S14050478x477",
					"Q" : "M515x508S1f050486x493",
					"R" : "M507x507S11a20492x477",
					"S" : "M507x507S20320492x492",
					"T" : "M508x507S1fb20493x488",
					"U" : "M507x507S11520492x477",
					"V" : "M508x507S10e20493x477",
					"W" : "M509x507S18620491x478",
					"X" : "M515x545S10620494x485S10640498x519",
					"Y" : "M514x507S19a20486x487",
					"Z" : "M516x531S10020490x470S2450a485x513"
				},
				keyMap: "qwerty", // According to Andre L, they don't use
				                      // the AZERTY keyboard in Quebec.
				localeGuesses: [ "fr-CA", "fr-QC" ]
			},
			"tse" : { // Tunisian Sign Language
				label: "Tunisian", // Dunno
				code: "tse",
				letters: {
					"ض" : "M510x508S1f720490x493",
					"ص" : "M508x508S20320493x493",
					"ث" : "M509x515S18720491x486",
					"ق" : "M512x511S12920488x489",
					"ف" : "M512x510S1eb20488x491",
					"غ" : "M515x512S11e02485x489",
					"ع" : "M515x508S11502485x493",
					"ه" : "M508x508S17620492x492",
					"خ" : "M513x508S18510488x493",
					"ح" : "M512x508S18210489x493",
					"ج" : "M509x510S16d10492x490",
					// "د" : "M513x510S1ed10487x491", // On top of the fingerspell toggle.
					"ذ" : "M512x512S12810489x488",
					"ش" : "M512x516S14c20489x485",
					"س" : "M506x514S15a20494x487",
					"ي" : "M514x510S19a20486x490",
					"ب" : "M508x515S10020493x485",
					"ل" : "M512x515S1dc20488x485",
					"ا" : "", // Can not reproduce symbol. Thumb up in a weird direction.
					"ت" : "M508x515S11520493x485",
					"ن" : "M510x512S1ec16491x489",
					"م" : "M511x510S19220490x491",
					"ك" : "M507x511S14720493x489",
					"ط" : "M511x515S10510489x485",
					// "ئ" : "", // Not in the list
					// "ء" : "", // Not in list
					// "ؤ" : "", // Waw with a dot. Not found.
					"ر" : "M513x513S10a20487x487",
					// "لا" : "", // Laamalif. Not found.
					// "ى" : "", // Also not found...
					// "ة" : "", // Not found
					"و" : "M509x511S17522492x490",
					"ز" : "M508x515S11d20492x485",
					"ظ" : "M515x515S14020486x485"
				},
				letterMap: {
					// RTL makes this display go crazy...
					48: "à", 65: "ش", 66: "لا", 67: "ؤ", 68: "ي", 69: "ث",
					70: "ب", 71: "ل", 72: "ا", 73: "ه", 74: "ت", 75: "ن", 
					76: "م", 77: "ة", 78: "ى", 79: "خ", 80: "ح", 81: "ض", 
					82: "ق", 83: "س", 84: "ف", 85: "ع", 86: "ر", 87: "ص", 
					88: "ء", 89: "غ", 90: "ئ", 186: "ك", 187: "=",
					188: "و", 189: ")", 190: "ز", 191: "ظ", 192: ">", 219:
					"ج", 220: "ذ", 221: "د", 222: "ط"
				},
				keyMap: "qwerty", // Apparently...
				localeGuesses: [ "ar-TN", "ar" ]
			},
			"sfb" : { // French Belgian Sign Language
				label: "LSFB",
				code: "sfb",
				letters: {
					"A" : "M512x508S1f520488x493",
					"B" : "M507x511S14710493x489",
					"C" : "M509x510S16d10492x490",
					"D" : "M508x515S10110492x485",
					"E" : "M508x509S14910492x491",
					"F" : "M515x514S1d210486x486",
					"G" : "M513x525S1ed50487x506S22e00492x484S2f700494x475",
					"H" : "M515x520S14020486x480S2f700494x513",
					"I" : "M511x510S19210490x491",
					"J" : "M524x514S19a20476x487S2ef02506x495",
					"K" : "M517x527S14020484x473S2df04496x506",
					"L" : "M512x515S1dc20488x485",
					"M" : "M512x508S14b50489x493",
					"N" : "M511x513S11950489x487",
					"O" : "M508x508S17610492x492",
					"P" : "M515x532S14020486x502S26900493x481S2f700496x469",
					"Q" : "M515x517S1ef50486x483S22a04498x502",
					"R" : "M507x507S11a20492x477",
					"S" : "M508x508S20310493x493",
					"T" : "M512x511S1ea10489x490",
					"U" : "M508x515S11520493x485",
					"V" : "M508x515S10e20493x485",
					"W" : "M509x515S18620491x485",
					"X" : "M515x522S10019485x492S10011494x492S20500496x479",
					"Y" : "M514x519S19a20486x482S22a04495x504",
					"Z" : "M524x520S10050476x481S2450a493x502"
				},
				localeGuesses: [ "fr-BE" ]
			},
			"bzs" : { // Brazilian Sign Language
				label: "LIBRAS",
				code: "bzs",
				letters: { // Should this have numbers and stuff?
					"A" : "M508x508S1f720488x493",
					"B" : "M507x509S14720493x487",
					"C" : "M504x509S16d20487x489",
					"D" : "M508x508S10120492x478",
					"E" : "M507x508S14a20492x493",
					"F" : "M510x508S1d220481x480",
					"G" : "M507x507S1de20485x477",
					"H" : "M512x532S14027488x479S2df04490x511",
					"I" : "M513x507S19220492x488",
					"J" : "M513x536S19220492x488S2a20c489x508",
					"K" : "M509x528S14020479x477S26904493x510",
					"L" : "M507x507S1dc20483x477",
					"M" : "M507x513S18d52482x493",
					"N" : "M507x515S11952481x493",
					"O" : "M508x508S17620492x492",
					"P" : "M509x511S14051478x487",
					"Q" : "M508x524S1f252490x494",
					"R" : "M507x508S11a20492x478",
					"S" : "M508x508S20320493x493",
					"T" : "M510x508S1d320481x480",
					"U" : "M507x508S11520492x478",
					"V" : "M507x508S10e20492x478",
					"W" : "M508x508S18620490x478S22a00492x461",
					"X" : "M507x529S10a40492x481S26504493x514",
					"Y" : "M513x529S19a20485x488S26904492x511",
					"Z" : "M525x507S2450a494x460S10050492x477",
					"Ç" : "M523x516S16d10478x489S2e200503x484",
					"´" : "M511x516S10040496x486S22a03490x484"
					// There's supposed to be a lot more stuff here, but I can't
					// yet figure out how to add it. I think I'll probably need
					// some shift+ stuff in fingerspelling.
					// "~" : ''
				},
				letterMap : {
					186 : "Ç",
					219 : "´"
				},
				keyMap: "qwerty", // well, sort of...
				localeGuesses: [ "pt-BR", "pt" ]
			},
			"dsl" : { // Danish Sign Language
				label: "DTS",
				code: "dsl",
				letters: {
					"A" : "M510x508S1f720490x493",
					"B" : "M507x511S14720493x489",
					"C" : "M509x510S16d20492x490",
					"D" : "M508x515S10120492x485",
					"E" : "M508x508S14a20493x493",
					"F" : "M509x515S1d820492x486",
					"G" : "M515x508S1f000486x493",
					"H" : "M515x508S11502485x493",
					"I" : "M511x510S19220490x491",
					"J" : "M519x520S19220498x501S2a20c482x481",
					"K" : "M515x515S14020486x485",
					"L" : "M512x515S1dc20488x485",
					"M" : "M510x513S18d50490x488",
					"N" : "M511x513S11950489x487",
					"O" : "M508x508S17620492x492",
					"P" : "M515x508S10112485x492",
					"Q" : "M515x512S1f121485x489",
					"R" : "M508x515S11a20493x485",
					"S" : "M508x508S20320493x493",
					"T" : "M515x509S1f220485x491",
					"U" : "M508x515S11520493x485",
					"V" : "M508x515S10e20493x485",
					"W" : "M509x515S18720491x486",
					"X" : "M511x513S10620490x487",
					"Y" : "M514x510S19a20486x490",
					"Z" : "M519x518S10020481x488S2450a488x482"
				},
				localeGuesses: [ "da" ]
			},
			"bfi" : { // British Sign Language
				label: "BSL",
				code: "bfi",
				letters: {
					"A" : "M514x517S14c09488x490S20500487x483",
					"B" : "M521x515S1ce18479x485S1ce10499x485",
					"C" : "M512x510S1ec10489x491",
					"D" : "M515x518S10018486x488S1ec10492x482",
					"E" : "M513x521S14c09487x494S20500497x479",
					"F" : "M519x518S20320504x503S20328481x483S20b00502x485",
					"G" : "M508x517S20300493x502S20308493x484",
					"H" : "M523x524S15a39477x501S15a51478x501S20e00499x489S26507510x477",
					"I" : "M517x518S14c09484x491S20500507x482",
					"J" : "M521x517S14c08480x485S10012491x483S22a04507x502",
					"K" : "M515x521S10018485x491S10613487x480",
					"L" : "M520x517S15a39480x484S10051490x496",
					"M" : "M518x517S15a39483x483S18c51492x491",
					"N" : "M518x517S15a39482x484S11551491x492",
					"O" : "M519x514S14c09481x487S20500509x492",
					"P" : "M513x525S1ce10491x475S10018487x495",
					"Q" : "M524x525S1ce18477x476S10620493x499S20700506x483",
					"R" : "M516x518S15a39485x483S10611493x490",
					"S" : "M516x519S19220485x500S1920a487x481S20800506x486",
					"T" : "M520x518S15a49481x483S10051490x497",
					"U" : "M518x514S14c09482x487S20500508x501",
					"V" : "M518x517S15a39483x484S10e51488x492",
					"W" : "M525x523S14c18476x492S14c10502x492S20700490x478",
					"X" : "M516x515S10011495x485S10019484x485",
					"Y" : "M515x527S15d28485x473S10020500x497S20800504x489",
					"Z" : "M519x518S15a18482x483S18210496x503"
				},
				localeGuesses: [ "en-GB" ]
			},
			"fsl" : { // French Sign Language
				label: "LSF",
				code: "fsl",
				letters: {
					"A" : "M510x508S1f720490x493",
					"B" : "M507x511S14720493x489",
					"C" : "M509x510S16d20492x490",
					"D" : "M508x515S10120492x485",
					"E" : "M508x508S14a20493x493",
					"F" : "M515x514S1d220486x486",
					"G" : "M511x515S1de20489x485",
					"H" : "M508x515S10e10493x485",
					"I" : "M511x510S19220490x491",
					"J" : "M518x519S19220497x500S2a20c482x482",
					"K" : "M515x515S14020486x485",
					"L" : "M512x515S1dc20488x485",
					"M" : "M513x510S18d52488x490",
					"N" : "M513x511S11952487x489",
					"O" : "M508x508S17620492x492",
					"P" : "M516x512S14021485x488",
					"Q" : "M515x511S1df03485x490",
					"R" : "M508x515S11a20493x485",
					"S" : "M508x508S20320493x493",
					"T" : "M515x514S1d320486x486",
					"U" : "M508x515S11520493x485",
					"V" : "M508x515S10e20493x485",
					"W" : "M509x515S18720491x486",
					"X" : "M510x514S11020491x487",
					"Y" : "M514x518S19a20486x498S22a00494x483",
					"Z" : "M519x519S10020482x489S2450a488x482",
					"Ù" : "M511x511S2df00490x490"
				},
				keyMap: "azerty",
				localeGuesses: [ "fr", "fr-FR" ]
			},
			"psr" : { // Portuguese Sign Language
				label: "LGP",
				code: "psr",
				letters: {
					"A" : "M507x507S14110478x486",
					"B" : "M507x508S1f502492x484",
					"C" : "M504x509S16d10487x489",
					"D" : "M511x521S20500494x510S15d32484x487",
					"E" : "M507x508S1ea10484x487",
					"F" : "M504x508S1d810487x479",
					"G" : "M507x508S20310492x493",
					"H" : "M504x508S1d610490x479",
					"I" : "M507x508S19210486x489",
					"J" : "M514x512S15f10494x485",
					"K" : "M509x531S14010479x478S22e04492x513",
					"L" : "M507x507S1dc20483x477",
					"M" : "M508x508S18d20488x483",
					"N" : "M507x508S11920486x482",
					"O" : "M508x508S17610492x492",
					"P" : "M507x512S1da02478x496",
					"Q" : "M510x527S22a03492x513S1f521488x490",
					"R" : "M507x507S11610492x475",
					"S" : "M507x508S14a10492x493",
					"T" : "M507x508S1dc02477x484",
					"U" : "M507x508S11500492x478",
					"V" : "M507x508S10e00492x478",
					"W" : "M507x508S18c00492x478S22a00493x462",
					"X" : "M507x508S11a20492x478",
					"Y" : "M515x527S22a04493x512S19a00487x488",
					"Z" : "M543x520S2450a512x502S10a10492x482"
				},
				letterMap : {
					186 : "Ç"
				},
				keyMap: "qwerty",
				localeGuesses: [ "pt", "pt-pt" ]
			}
		},
		// This might be removable after switching to truetype
		svgSupported = ( function () {
			var div = document.createElement('div');
			div.innerHTML = '<svg/>';
			if ( div.firstChild && 
				div.firstChild.namespaceURI === "http://www.w3.org/2000/svg" 
			) {
				return "svg";
			} else {
				return "png";
			}
		})(),
		writingModeSupported = ( 
			'-webkit-writing-mode' in signListElem.style || 
			'-moz-writing-mode' in signListElem.style || 
			'-ms-writing-mode' in signListElem.style || 
			'writing-mode' in signListElem.style
		),
		animationsSupported = ( 
			'-webkit-animation-name' in signListElem.style || 
			'-moz-animation-name' in signListElem.style || 
			'-ms-animation-name' in signListElem.style || 
			'animation-name' in signListElem.style
		),
		transformSupported = ( 
			'-webkit-transform' in signListElem.style || 
			'-moz-transform' in signListElem.style || 
			'-o-transform' in signListElem.style || 
			'-ms-transform' in signListElem.style || 
			'transform' in signListElem.style
		),
		// Note: textContent might actually be faster than innerText. TODO.
		innerText = "innerText" in signListElem ? "innerText" : "textContent",
		symbolServer = "//swis.wmflabs.org/glyph.php?font=" + svgSupported + "&key=",
		signServer   = "//swis.wmflabs.org/glyphogram.php?font=" + svgSupported + "&text=",
		//"&line=000000&fill=ffffff&size=0.43333333333333335"
		caret,
		disabled = false,
		console = window.console || {
			log: function () {},
			error: function () {}
		};
	
	// ** REGULAR EXPRESSIONS **
	// In progress: Moving these to fswParser.
	var symbolReg = /^(S[123][0-9a-f]{2}[0-5][0-9a-f])([0-9]{3})x([0-9]{3})$/,
		spellingSequencePattern = "A(?:S[123][0-9a-f]{2}[0-5][0-9a-f])+",
		spellingSequenceReg = new RegExp( "^" + spellingSequencePattern );
	
	/**
	 * @enum {number}
	 */
	var symbolTypes = {
		"NONE" : 0,
		"HAND" : 1,
		"MOVEMENT" : 2,
		"DYNAMICS" : 3,
		"HEAD" : 4,
		"BODY" : 5,
		"PUNCTUATION" : 6,
		"UNSPECIFIED" : 7
	};
	
	// TODO: Rename
	var generalActions = ( function () {
		// TODO: Rename.
		// Note: All of these return other functions.
		var actionGenerators = {
			// Rotate symbol.
			rotate: function rotate( direction ) {
				return function ( rightHand, position, icon, shiftKey ) {
					var timer, first = true, symbol = this, fake = this.fake,
						change = direction * ( shiftKey ? 2 : 1 );
					
					( function rotateLoop() {
						symbol.rotation = ( symbol.rotation + change + 8 ) % 8;
						if ( fake !== true ) {
							timer = setTimeout( rotateLoop, first ? 200 : 100 );
						}
						if ( first ) {
							first = false;
						} else {
							symbol.updateImage();
							keyboard.update( rightHand );
						}
					} )();
					
					if ( fake !== true ) {
						keyboard.keyupActions[ position ] = function () {
							clearTimeout( timer );
						};
					}
				};
			},
			// Change symbol position. 
			move: function move( X, Y ) {
				return function ( rightHand, position, icon, shiftKey ) {
					/*
					if ( this.fake ) {
						return;
					}
					*/
					
					/*
					350ms = 1
					1500ms = 200px
					
					*/
					var time = Date.now(), req, symbol = this, distance = 0,
						//base = 800, power = 3;
						modifier = shiftKey ? 10 : 1,
						base = 1200, power = 3, speedPeak = 0.4; // 0.5
					
					( function moveLoop() {
						var newTime = + new Date(),
							timeDiff = newTime - time, 
							progress = timeDiff / base * 0.5,
							d;
						if ( timeDiff < 0 ) {
							//console.log( "????", timeDiff, newTime, time, d );
						}
						
						if ( progress >= 1 ) { // grind to a stop
							return;
						}
						
						d = ( progress < 0.5 ? 
							Math.pow( 2, power - 1 ) * Math.pow( progress, power ) : 
							1 + Math.pow( 2, power - 1 ) * Math.pow( progress - 1, power ) )
							* base * modifier;
						
						d = Math.max( d, modifier );
						
						/*
						if ( !( timeDiff <= base / 2 ) ) {
							if ( !timeDiff ) {
								//console.log( "?", timeDiff )
							}
						}
						*/
						
						symbol.X += ( Math.ceil( d ) - distance ) * X;
						//symbol.X = Math.max( minX, Math.min( maxX - ( symbol.width || 0 ), symbol.X ) );
						
						// symbol.Y += Math.round( Y * timeDiff * 0.1 );
						symbol.Y += ( Math.ceil( d ) - distance ) * Y;
						// TODO: Move this to updatePosition?
						//symbol.Y = Math.max( minY, Math.min( maxY - ( symbol.height || 0 ), symbol.Y ) );
						symbol.fitToConstraints();
						
						// Don't call updatePosition unless the position has 
						// actually changed.
						if ( Math.ceil( d ) !== distance ) {
							symbol.updatePosition();
						}
						
						distance = Math.ceil( d );
						
						req = requestAnimationFrame( moveLoop );
					} )();
					keyboard.keyupActions[ position ] = function () {
						cancelAnimationFrame( req );
					};
				};
			},
			// TODO: Do something visible to the key for the current lane.
			switchLane: function switchLane( lane ) {
				return function () {
					// Bug: The M still gets symbols left in it after switching from
					// a Sign that was on fingerspelling mode.
					var sign = this.sign;
					sign.lane = lane;
					sign.updatePosition();
				};
			}
		};
		
		return {
			rotateLeft:  actionGenerators.rotate(  1 ), // Rotate counter-clockwise
			rotateRight: actionGenerators.rotate( -1 ), // Rotate clockwise
			togglePlane: function ( rightHand ) {
				this.plane ^= 1;
			},
			toggleBothHands: function ( rightHand ) {
				this.bothHands ^= 1;
			},
			leftLane:   actionGenerators.switchLane( "L" ),
			middleLane: actionGenerators.switchLane( "M" ),
			rightLane:  actionGenerators.switchLane( "R" ),
			moveUp:    actionGenerators.move(  0, -1 ),
			moveRight: actionGenerators.move(  1,  0 ),
			moveDown:  actionGenerators.move(  0,  1 ),
			moveLeft:  actionGenerators.move( -1,  0 ),
			faceLoop: function ( rightHand, position, alt, shiftKey ) {
				this.face = ( 
					this.face + 
					4 + 
					// Other direction when the shift key is held.
					( shiftKey ? -1 : 1 )
				) % 4;
			},
			faceToggle: function ( rightHand, position, alt ) {
				this.face ^= 1;
			}
		};
	} )();
	
	// TODO: Documentation
	var extraKeys = {
		8 : deleteSign( -1 ), // backspace
		13: function () {     // enter key
			// Either submit or togglePlaintext, line break, and re-toggle
			// Actually, maybe there should be a separate "Linebreak" class?
			// Dunno.
			if ( signArea.textBoxType ) {
				if ( signArea.textBoxType === "INPUT" ) {
					// Submit
					activeSign.centerSign();
					signArea.updateTextboxContent();
					signArea.textBox.form && signArea.textBox.form.submit();
					return true;
				} else {
					// Insert line break
					// Okay, this is complicated.
					caret.togglePlaintext();
					plainTextExtraKeys[ 13 ]();
					caret.togglePlaintext();
				}
			}
		},
		32 : spaceKey,       // space key
		38 : function () {   // up arrow
			signArea.changeSign( activeSign.index() - 1, true );
			if ( !activeSign.isSign ) {
				// Move caret to the end.
				caret.selectElement( activeSign.element, true, true );
				return false;
			}
			// TODO: Check if this messes things up in some browsers.
			// Hm, seems this moves the caret too much and thus blurs the SL.
			//return true;
		},
		39 : function ( keyCode, event ) { // right arrow
			return;
			// Doesn't work yet.
			// Might use document.elementFromPoint
			//caret.move( 1, event, "line" );
			/*
			// Okay, this didn't work as expected.
			if ( "modify" in getSelection() ) {
				caret.escapeSign();
				getSelection().modify( "move", "forward", "line" );
				caret.keepInView();
			}
			*/
			// There exists proprietary functions for range from point in:
			// * FF20+ (document.caretPositionFromPoint)
			// * Chrome, Safari 5+ (document.caretRangeFromPoint)
			// * IE old< (TextRange.moveToPoint)
			// No support for modern IE.
			
			var coords = activeSign.element.getBoundingClientRect(),
				found,
				foundIndex;
			console.log( coords );
			found = document.elementFromPoint( coords.right + 10, coords.top + 1 );
			if ( found ) {
				for ( ; found.parentNode && found.parentNode !== signArea.element; ) {
					found = found.parentNode;
				}
				if ( found.parentNode === signArea.element ) {
					for( var i = 0; i < signArea.signs.length; i++ ) {
						if ( signArea.signs[ i ].element === found ) {
							signArea.changeSign( i );
							//caret.keepInView();
							return;
						}
					}
				}
			}
			
		},
		40 : function () {   // down arrow
			signArea.changeSign( activeSign.index() + 1, true );
			if ( !activeSign.isSign ) {
				// Is this necessary?
				caret.selectElement( activeSign.element, false, true );
				return false;
			}
			//return true;
		},
		46 : deleteSign( 1 ),
		/*
		// These aren't working right...
		36: function () { // Home key
			caret.escapeSign();
			return true;
		},
		35: function () { // End key
			caret.escapeSign();
			return true;
		},
		*/
		192: function () { // ` key
			caret.togglePlaintext();
		}
	},
	// These only apply while the activeSign is a nonSignText object.
	plainTextExtraKeys = {
		8 : deleteText( -1 ), // Backspace key
		46: deleteText(  1 ), // Delete key
		13: function ( keyCode, e ) { // Enter key
			// IE turns things into paragraphs on enter by default.
			
			// NOT YET DONE.
			
			if ( signArea.textBoxType === "INPUT" ) {
				// Submit. Same as extraKeys[ 13 ].
				signArea.textBox.form && signArea.textBox.form.submit();
				return;
			}
			
			// Attempt 3:
			var range = caret.getRange(),
				caretHolder;
			
			// Okay, some issues here:
			// 1. This is not the only time we need to add a caretHolder. 
			//     Examples: Creating new NST that ends in \n, backspace to \n,
			//               deleting after \n, pasting NST, etc.
			// 2. We don't always need to add a caretHolder here.
			//     Example: There's already content after, and we're adding to 
			//             the middle of the NST.
			// 3. Leaving cH around when not in use might be inconvinient, but
			//    I can't currently think of how.
			// 4. ...
			
			// TODO: Add maintain() to initialization function.
			// TODO: Fix styling problems with IE. width:100% means something
			// rather different when writing mode isn't on.
			/*
			activeSign.caretHolder = caretHolder = 
				( activeSign.caretHolder && activeSign.maintain() ) ||
					document.createElement( "span" );
			
			caretHolder.className = "SWKB-caretHolder";
			range.insertNode( caretHolder );
			range.selectNodeContents( caretHolder );
			// simple selectNodeContents doesn't work. setStart doesn't work.
			//range.selectNode( 
				//caretHolder.appendChild( document.createTextNode( "div" ) ) );
			//caretHolder.removeChild( caretHolder.firstChild );
			*/
			// Replacing above with more maintain()
			// Do I actually need the first maintain? Trying without.
			//activeSign.maintain();
			var br;
			range.insertNode( br = document.createElement( "br" ) );
			// Wait, I didn't move the caret if the br already has stuff after it.
			activeSign.maintain();
			// Maybe
			range.setStartAfter( br );
			range.setEndAfter( br );
			// Okay, we still have a few problems here:
			// * .maintain() moves the caret.
			// * Actually, *.maintain might need to be called more often. Some 
			//   things aren't prepared to deal with non-textnodes.
			getSelection().removeAllRanges();
			getSelection().addRange( range );
			activeSign.maintain();
			// IT WORKS!! Wow, I am so relieved that this mess finally went 
			// somewhere.
			
			// Under ordinary circumstances, Enter>Enter>Backspace>Enter results
			// in three line breaks, because he backspace somehow results in the
			// line break being added to  the caretHolder instead of being
			// deleted. To fix this, we've got a whole caret.midDeletion thing
			// making sure to clear the caretHolder of the linebreak after the
			// backspace or delete key is pressed.
			return false;
			
			// document.execCommand( "insertHTML", false, "<br />" );
			// document.execCommand( "insertText", false, "a\na" );
			// document.execCommand( "insertBrOnReturn", false, true );
			//return true;
			//window.getSelection().getRangeAt(0).insertNode( document.createElement( "br" ) );
			
			// Okay, if I'm understanding this correctly:
			// Chrome will remove one of these brs as soon as we start typing
			// text after it. It will be removed from the DOM.
			// However, adding a Sign after it won't remove it.
			// Without this br, keeping the caret in the right spot is not
			// possible. 
			// If there was already text later, the extra br won't be removed.
			// Tested: Adding two line breaks and then doing anything before
			// both line breaks wills result in the later one being removed.
			// Tested again, this time it doesn't happen. ???
			// Okay, this is messed up. Under certain conditions, backspace can
			// cause a new linebreak to show up.
			// Text nodes will merge with other text. Do not use invisible text
			// instead of an extra br.
			// IE will leave both brs in, and count them both as \ns.
			
			// Maybe have the NST have a property indicating whether there's an
			// extra line break somewhere?
			/*
			var selection = window.getSelection(), 
				range,
				div = $( "<div contenteditable='true'>a<br>b<br></div>" ).appendTo( "body" )[ 0 ];
			selection.removeAllRanges();
			range = document.createRange();
			range.selectNodeContents( div );
			// range.setStartBefore( div.firstChild );
			// range.setEndAfter( div.lastChild );
			console.log( range.cloneContents().childNodes.length );
			*/
			
			var selection = window.getSelection(), 
				range = selection.getRangeAt(0),
				frag = document.createDocumentFragment(),
				newEle1,
				newEle2;
				//newEle = frag.appendChild( document.createTextNode( "\n" ) );
				//empty = frag.appendChild( document.createTextNode( "\u200b" ) );
			/*
			newEle1 = frag.appendChild( document.createElement( "br" ) );
			newEle = frag.appendChild( this.extraBr || document.createElement( "br" ) );
			range.deleteContents();
			range.insertNode( frag );
			
			this.extraBr = newEle;
			*/
			// attempt 2
			// Current status: Working on both IE and Chrome. Some bugs.
			// Double line break happens on odd occasions. Can't figure out why.
			activeSign.clean();
			newEle1 = document.createElement( "br" );
			range.insertNode( newEle1 );
			console.log( newEle1, newEle1.parentNode );
			if ( !newEle1.nextSibling || newEle1.nextSibling.nodeValue === "" ) {
				console.log( "Adding extraBr" );
				range.insertNode( newEle2 = activeSign.extraBr || document.createElement( "br" ) );
			} else {
				console.log( newEle1.nextSibling );
			}
			// For figuring out what is going on in the DOM. Need to remember
			// to remove these lines.
			newEle1.setAttribute( "zzz", "newEle1" );
			newEle2 && newEle2.setAttribute( "zzz", "newEle2" );
			// console.log( "range contains: ", range.cloneContents() );
			// range.deleteContents();
			activeSign.extraBr = newEle2;
			// end attempt 2
			range = document.createRange();
			range.setStartAfter( newEle2 || newEle1 );
			range.setEndAfter( newEle2 || newEle1 );
			range.collapse( true );
			caret.selectRange( range );
			//selection.removeAllRanges();
			//selection.addRange( range );
			range.detach();
			//console.log( newEle_, newEle );
			/*
			range.selectNodeContents( empty );
			selection.removeAllRanges();
			selection.addRange( range );
			*/
			//text.parentNode.removeChild( text );
			
			//return true;
			
		},
		// Simple arrow keys.
		37: function ( keyCode, event ) { // left arrow
			caret.move( -1, event, writingModeSupported ? "line" : "character" );
		},
		38: function ( keyCode, event ) { // up arrow
			caret.move( -1, event, writingModeSupported ? "character" : "line" );
		},
		39: function ( keyCode, event ) { // right arrow
			caret.move( 1, event, writingModeSupported ? "line" : "character" );
		},
		40: function ( keyCode, event ) { // down arrow
			caret.move( 1, event, writingModeSupported ? "character" : "line" );
		},
		192: function () { // ` key
			caret.togglePlaintext();
		},
		// Chrome explodes if you press pgup/pgdn while a mixed-display
		// contenteditable element is focused.
		33: function () { // Page up
			return false;
		},
		34: function () { // Page down
			return false; 
		}
		/*
		90: function () { // z key, for testing
			// wpTextb
			//activeSign.normalize();
			//activeSign.maintain();
			return false;
		}
		*/
	};
	
	// WELCOME TO YAIR RAND'S BIG PILE O' BADLY UNDERSTOOD MISCELLANEOUS SIGNWRITING DATA
	// Actual code continues roughly 1300 lines down.
	// I should probably move this all into BaseLayout.
	
	// Maybe merge these two into an object.
	var handHeels = [ "14d", "14f", "151", "15c", "15e", "1f6", "204" ],
	
	// This was for the Between Palm Facings handshapes. No longer used.
	handBetweenFacings = { "15a" : "15b" },
	
	// All hand positions.
	// Most people probably assume that we have some kind of database on the 
	// details of each hand shape's fingers. Nope. This is actually a list of
	// what each symbol should switch to for each key and combination of keys.
	// ¯\_(ツ)_/¯
	handShapes = {
		// To consider: Attaching the cups to the circles.
		// To consider: Loading ring's alts with random common handshapes.
		//   Maybe cup, closed hinge, and one other common
		"" : ["0"],
		//       Small,  Ring, Middle, Index, Thumb, Alt1(C),
		"0"   : [ "192", "1ae", "1c6", "100", "1f7" ],
		// ** Index
		"100" : [ "1a0", "1b7", "10e", "10b", "1de", "101", "106", "15a" ], // Index, and following
		"10b" : [ "1a0", "1b7", "114", "10c", "1e3", "10d",      , "181" ], // hinge index
		"10c" : [ "1a0", "1b7", "114", "10a", "1e3", "10d",      , "182" ], // lower hinge index
		"10a" : [ "1a0", "1b7", "118", "106", "1ed",      ,      , "171" ], // "cup" index
		"106" : [ "1a0", "1b7", "116", "109", "1e1", "107",      , "108" ], // bent index
		"109" : [ "1a0", "1b7", "111", "203", "1de",      ,      , "202" ], // raised knuckle index 
		"101" : [ "1a1", "1b7", "10f", "10d", "1de", "100", "102" ], // Circle - Index, and following. Not done.
		"10d" : [ "1a1", "1b7", "10f", "107", "1e3", "10b" ], // Circle - hinge index
		"107" : [ "1a1", "1b7", "10f", "176", "1e1", "106" ], // Circle - bent index
		"108" : [ "1a0", "1b7", "116", "201", "1e1", "107",      , "106" ], // bent index, thumb under
		// "100" : [ "1a0", "1b7", "10e", "10b", "1de" ], // duplicate. to be removed.
		// ** Index Middle Ring Baby (All but thumb.)
		"144" : [ "186", "1a4", "1ba", "1cd", "14c", "145", "146", "147" ],
		"144-" : { 15 : "146" },
		"147" : [ "18c", "148", "148", "1cd", "15d", "149", "14b", "144" ], // Unit
		"147-" : { 3 : "115", 15 : "14b" },
		"148" : [ "18c", "147", "147", "1cd", "163",      ,      , "144" ], // Kohen
		"146" : [ "18d", "1a4", "1ba", "1cd", "158", "145", "145", "14b" ], // Hinge spread
		"146-" : { 3 : "112", 7 : "10c", 15 : "145" },
		"14b" : [ "18d", "1a4", "1ba", "1cd", "180", "147", "149", "146" ], // Hinge unit
		"14b-" : { 3 : "119", 7 : "10c", 15 : "149" },
		"149" : [ "18d", "1a4", "1ba", "1cd", "167", "14b", "14a", "145" ], // Claw unit
		"145" : [ "18b", "1a4", "1ba", "1d5", "14e", "146", "144", "149" ], // All bent
		"145-" : { 3 : "110", 15 : "202" },
		// If necessary, use alt1, currently redundant.
		// ** Index Middle Ring
		"186" : [ "144", "10e", "1b7", "1b4", "18f", "187", "18b", "18c" ], // Base
		"186-" : { 14 : "18d" },
		"18c" : [ "147", "115", "1b7", "1b5", "18e", "187", "18d", "186" ], // Unit
		"18c-" : { 14 : "18d" },
		"18b" : [ "145", "110", "1b7", "1b4", "18f", "187", "186", "18d" ], // All bent. 
		"18b-" : { 14 : "203" },
		"187" : [ "144", "10f", "1b7", "1b4", "18f", "186" ], // Circle
		"18d" : [ "14b", "119", "1b7", "1b5", "18e", "187", "18c", "18b" ], // Unit hinge
		"18d-" : { 14 : "18b" },
		// ** Baby 
		"192" : [ "198", "1b0", "1cc", "1a0", "19a", "194", "193" ],
		"198" : [ "197", "1b0", "1cc", "1a0", "199", "145" ], // Bent
		"197" : [ "203", "1b0", "1cc", "1a0", "19a",      ,      , "202" ], // Knuckle
		"194" : [ "176", "1b1", "1cc", "1a1", "19a", "192", "195" ], // Circle
		"194-" : { 6: "1ce" },
		"193" : [ "201", "1b0", "1cc", "1a0", "19a", "194", "192" ], // thumb under
		// "192" : [ "203", "1b0", "1cc", "1a0", "19a" ],
		// ** None. (Fist.)
		"203" : [ "192", "1ae", "1c6", "100", "1f7", "176",      , "204" ],
		"204" : [ "192", "1ae", "1c6", "100", "1f6", "176",      , "203" ], // Heel
		"176" : [ "191", "1ae", "1c7", "101", "174", "203" ], // Circle
		"176-" : { 3 : "129", 7 : "1eb" },
		"1ff" : [ "197", "1af", "1c8", "109", "14a", "1fe", "202", "1fb" ], // Thumb over index middle
		"14a" : [ "197", "1af", "1c8", "109", "1f7",      , "147" ], // Claw unit bent
		// Much of ^this one should be redone. 
		// Maybe should have knuckle links like above.
		"202" : [ "192", "1ae", "1c6", "100", "1f7", "201", "1f7", "14a" ], // Knuckles
		"202-" : { 3 : "111", 7 : "109", 9 : "1b6", 11 : "1c8", 13 : "1af", 14 : "197", 15 : "203" },
		// ** Ring
		"1ae" : [ "1b0", "1af", "1b4", "1b7", "1b8",      , "185", "16d" ],
		// Some random shortcuts to unrelated common handshapes. ^
		"1af" : [ "1b0", "203", "1b6", "1b7", "1b9",      ,      , "202" ], // Knuckle
		// "1ae" : [ "1b0", "203", "1b4", "1b7", "1b8" ],
		// ** Ring Baby
		"1b0" : [ "1ae", "192", "1cd", "1ba", "001", "1b1", "1b2", "1b3" ],
		"1b0-" : { 3 : "203" },
		"1b1" : [ "1ae", "194", "1ce", "1bb", "001", "1b0" ], // Circle
		"1b1-" : { 3 : "176" },
		// ** Middle
		"1c6" : [ "1cc", "1b4", "1c8", "10e", "1c9", "1c7" ],
		"1c7" : [ "1cc", "1b4", "176", "10f", "1c9", "1c6" ], // Circle
		"1c7-" : { 3 : "1ce" },
		"1c8" : [ "1cc", "1b6", "203", "111", "1ca",      ,      , "202" ], // Knuckle
		// "1c6" : [ "1cc", "1b4", "203", "10e", "1c9" ],
		// ** Middle Baby
		"1cc" : [ "1c6", "1cd", "192", "1a4", "1cb" ],
		// ** Middle Ring
		"1b4" : [ "1cd", "1c6", "1ae", "186", "002",      , "1b6", "1b5" ],
		"1b5" : [ "1cd", "1c6", "1ae", "18c", "002",      ,      , "1b4" ], // Unit
		"1b6" : [ "1cd", "1c8", "1af", "186", "002",      , "1b4", "202" ], // Knuckle
		// "1b4" : [ "1cd", "1c6", "1ae", "186", "000" ],
		// ** Middle Ring Baby
		"1cd" : [ "1b4", "1cc", "1b0", "144", "1d7", "1ce" ],
		"1ce" : [ "1b4", "1cc", "1b1", "144", "1cf", "1cd", "1d5" ], // Circle
		"1ce-" : { 3 : "1c7", 6 : "194", 7 : "1d5" },
		"1d5" : [ "1b4", "1cc", "1b1", "145", "1cf", "1cd", "1ce" ], // Circle, bent
		"1d5-" : { 7 : "176" },
		"1cf" : [ "1b4", "1cc", "1b1", "144", "1d7", "1cd" ], // Curlicue
		// ** Index Baby
		"1a0" : [ "100", "1ba", "1a4", "192", "19c", "1a1" ],
		"1a1" : [ "101", "1bb", "1a5", "194", "19c", "1a0" ], // Circle
		// ** Ring Index
		"1b7" : [ "1ba", "100", "186", "1ae", "003" ],
		// ** Index Ring Baby
		"1ba" : [ "1b7", "1a0", "144", "1b0", "1c4", "1bb" ],
		"1bb" : [ "1b7", "1a1", "152", "1b1", "1bc", "1ba" ], // Circle
		"1bb-" : { 3 : "101" },
		// ** Index Middle
		"10e" : [ "1a4", "186", "113", "114", "11e", "10f", "11a", "115" ],
		"114" : [ "1a4", "186", "112", "116", "126" ], // Index pressed  (hinge)
		"113" : [ "1a4", "186", "117", "112", "124", "" ], // Middle pressed (hinge)
		"112" : [ "1a4", "18d", "10c", "1c6", "123", "110", "118", "119" ], // Both pressed (hinges). 
		"112-" : { 3 : "146", 12 : "118" },
		"116" : [ "1a4", "186", "110", "1c6", "131" ], // Index pressed*2 (bent)
		"117" : [ "1a4", "186", "100", "110", "130",      , "11c" ], // Middle pressed*2 (bent)
		"110" : [ "1a4", "18b", "106", "1c6", "121" ], // Both pressed*2 (bent)
		"110-" : { 12 : "111" },
		"118" : [ "1a4", "186", "10a", "114", "128" ], // Cup. Currently inaccessible except from 10a in Index section. 
		// ^ To consider: Replace 128 with 135. 135 has unit, but no thumb cup. Hm.
		"118-" : { 12 : "110" },
		"11a" : [ "1a9", "186", "11c", "114", "133", "11b", "10e", "11d" ], // Crossed
		"11c" : [ "1a9", "186", "113", "114", "133", "11b", "10e", "11d" ], // Crossed, middle bent
		"11d" : [ "1a4", "186", "113", "114", "11e", "10f", "115", "11a" ], // Crossed, reverse
		"111" : [ "1a4", "186", "109", "1c8", "11e",      ,      , "202" ], // Knuckles. Accessed from Index or Middle knuckle
		"10f" : [ "1a5", "187", "101", "1c7", "11f", "10e", "11b" ], // Circle
		// Not sure if crossed should be alt2 or alt3.
		"11b" : [ "1aa", "187", "101", "1c7", "133", "11a", "10f" ], // Circle, Crossed
		"115" : [ "1a4", "18c", "113", "114", "12d", "10f", "11d", "10e" ], // Unit
		"115-" : { 12 : "119" },
		"119" : [ "1a4", "18d", "10c", "1c6", "132", "10f",      , "112" ], // Unit hinge
		// ^ This should probably be considered associated with the crossed-over handshapes.
		"119-" : { 3 : "14b", 12 : "118" },
		//"10e" : [ "1a4", "186", "100", "1c6", "11e" ],
		// ** Index Middle Baby
		"1a4" : [ "10e", "144", "1a0", "1cc", "1ab", "1a5", "1a9" ],
		"1a5" : [ "10f", "144", "1a1", "1cc", "1ab", "1a4", "1aa" ], // Circle
		"1a9" : [ "11a", "144", "1a0", "1cc", "1ab", "1aa", "1a4" ], // Crossed
		"1aa" : [ "11b", "144", "1a1", "1cc", "1ab", "1a9", "1a5" ], // Crossed on Circle
		// ** Basic thumb
		"1f7" : [ "19a", "1b8", "1c9", "1de", "1f5", "1f6", "1ff", "1fe" ],
		"1f5" : [ "19a", "1b8", "1c9", "1dc", "1fa", "1f6",      , "14a" ], // Straight
		"1fa" : [ "19a", "1b8", "1c9", "1e4", "1f9", "174" ], // Forward
		"1fa-" : { 12 : "127", 15 : "152" },
		"1f9" : [ "19a", "1b8", "1c9", "1e0", "1f8" ], // Bent. Inaccessible except from 1e2 (index thumb)
		"1f9-" : { 12 : "120" },
		"1f6" : [ "19a", "1b8", "1c9", "1de", "1f5", "1f7" ], // Heel
		"1f6-" : { 15 : "14d" },
		"1f8" : [ "19a", "1b8", "1c9", "1df", "203",      ,      , "1f7" ], // Unit
		"1f8-" : { 12 : "12e", 15 : "15a" },
		// Many others should be associated with thumb side unit, probably
		// including either thumb over and/or under fingers and/or knuckles.
		// Also, all the Betweens (1fb, 1fc, 1fd).
		// Also, Middle thumb hook (1ca) and Ring thumb hook (1b9)
		// Unit thumb may need to be simple thumb movement.
		// "1fe" : [ "197", "1af", "1c8", "109", "200" ], // Under two fingers
		"1fe" : [ "193", "1fd", "1fc", "1fb", "200" ], // Under two fingers
		"200" : [ "193", "1fd", "1fc", "1fb", "201" ], // Under three fingers
		"201" : [ "193", "1fd", "1fc", "108", "203" ], // Under four fingers
		"1fb" : [ "19a", "1b8", "1ca", "1de", "1f5" ], // Between Index Middle
		"1fc" : [ "19a", "1b9", "1ca", "1de", "1f5" ], // Between Middle Ring
		"1fd" : [ "193", "1b9", "1c9", "1de", "1f5" ], // Between Ring Baby
		// These three not yet done.
		"174" : [ "19a", "1b8", "1c9", "1e4", "175" ], // Curlicue open
		"175" : [ "19a", "1b8", "1c9", "1e4", "176" ], // Curlicue
		//"1f7" : [ "19a", "1b8", "1c9", "1de", "203" ], // dup
		// ** Baby Thumb
		"19a" : [ "199", "001", "1cb", "19c", "199" ], // Basic. To consider: Thumb goes to 193. 
		"199" : [ "1f7", "001", "1cb", "19c", "192" ], // Contact. NOTE: IMAGES ARE DISTORTED. 
		// "19a" : [ "1f7", "000", "1cb", "19c", "192" ],
		// ** Ring Thumb
		"1b8" : [ "001", "1f7", "002", "003", "1ae", "1b9" ],
		"1b8-" : { 18 : "203" },
		"1b9" : [ "001", "1f7", "002", "003", "1af" ], // Thumb Hook Knuckle
		// "1b8" : [ "000", "1f7", "000", "000", "1ae" ],
		// ** BLAH. TODO: Something.
		"000" : [ "1c4", "1de", "18f", "1b8", "1b7" ],
		// ** Middle Thumb
		"1c9" : [ "1cb", "002", "1f7", "11e", "1c6" ],
		"1ca" : [ "1cb", "002", "1f7", "11e", "1c8" ], // Thumb hook knuckle
		// "1c9" : [ "1cb", "000", "1f7", "11e", "1c6" ],
		// ** Middle Baby Thumb
		"1cb" : [ "1c9", "1d7", "19a", "1ab", "1cc" ],
		// ** All but index, sort of.
		"1d7" : [ "002", "1cb", "001", "14c", "1d6" ],
		"1d6" : [ "002", "1cb", "001", "152", "1da",      ,      , "1d0" ], // Thumb index claw
		"1da" : [ "002", "1cb", "001", "1d9", "1d8", "1d4" ], // Thumb index hook
		"1d9" : [ "002", "1cb", "001", "152", "1cd", "1d3" ], // Hook, thumb inside
		"1d8" : [ "002", "1cb", "001", "152", "1cd", "1d2" ], // Hook, thumb outside
		// "1d7" : [ "000", "1cb", "000", "14c", "1cd" ],
		// ** Index thumb
		"1de" : [ "19c", "003", "11e", "1e3", "1dc", "1eb", "1f0", "1df" ], // Base
		"1de-" : { 24 : "1ee" },
		"1e1" : [ "19c", "003", "131", "1f7", "1e5" ], // Bent index (base). Inaccessible except from index (bent).
		"1e1-" : { 7 : "1db" },
		"1e5" : [ "19c", "003", "131", "1e7", "1e6" ], // Bent index (forward thumb). 
		"1e5-" : { 7 : "157" },
		"1e6" : [ "19c", "003", "131", "1f7", "1e2", "1e7", "1e8" ], // Bent index (hook thumb)). 
		"1e7" : [ "19c", "003", "131", "1fa", "1e6" ], // Bent index (forward thumb), curlicue. 
		"1e2" : [ "19c", "003", "122", "1f9", "106" ], // Bent index (bent thumb). 
		"1e2-" : { 7 : "150" },
		"1e3" : [ "19c", "003", "126", "1e1", "1ee", "1ed" ], // Index hinge. 
		// ^ Should maybe go to 1ef instead of 1ee? The cups (1ed, 1ec) are mostly inaccessible.
		"1e3-" : { 7 : "1db" },
		"1ee" : [ "19c", "003", "12b", "1ef", "10b",      ,      , "17b" ], // Index hinge, thumb forward.
		"1ee-" : { 7 : "1d1" },
		"1ef" : [ "19c", "003", "12b", "1ed", "1f0",      , "1f4", "17d" ], // Index hinge, thumb forward, lower index. 
		"1ef-" : { 7 : "1d1" },
		"1f0" : [ "19c", "003", "12b", "1ed", "1f1",      , "1f4", "17e" ], // Index hinge, thumb forward, small. 
		"1f0-" : { 7 : "1d1" },
		"1f1" : [ "19c", "003", "12b", "1ec", "1f4" ], // Index hinge, thumb forward, tiny. 
		"1f1-" : { 7 : "1d1" },
		"1f4" : [ "19f", "003", "13f", "1f3", "1f2",      ,      , "185" ], // Index hinge, contact. 
		"1f4-" : { 7 : "1d4" },
		// The hinges still have two left: 1f2 and 1f3, which are thumb out and in, respectively.
		// Perhaps these could be Thumb and Index buttons. Or alts.
		"1f2" : [ "19e", "003", "13e", "1fa", "10c", "1d8", "1d2", "183" ], // Index hinge, contact, thumb out. 
		"1f2-" : { 7 : "1d2" },
		"1f3" : [ "19f", "003", "12c", "1fa", "10c", "1e8", "1d3", "184" ], // Index hinge, contact, thumb in. 
		"1f3-" : { 7 : "1d3" },
		"1e0" : [ "19c", "003", "120", "1e2", "100" ], // Bent thumb. Only from thumb from thumb index.
		"1ed" : [ "19c", "003", "137", "1ec", "1eb",     , "16d" ], // Cup - Open
		"1ed-" : { 7 : "1d0" },
		"1ec" : [ "19c", "003", "137", "1e5", "1eb",     , "16d" ], // Cup
		"1ec-" : { 7 : "1d0" },
		"1eb" : [ "19c", "003", "13c", "1e7", "1ea", "1e8", "1e6" ], // Cup - contact
		// ^ Sort of messed up/inconsistent. TODO. 
		"1eb-" : { 7 : "1ce" },
		"1ea" : [ "19c", "003", "13c", "1fa", "10a", "1e8", "1e6" ], // Cup - thumb under
		"1e8" : [ "19c", "003", "13b", "1fa", "10a", "1ea", "1e6", "1e9" ], // Cup - thumb inside
		"1dc" : [ "19c", "003", "11e", "1e3", "1e4" ], // Straight thumb. (Sideways, not forward.)
		"1e4" : [ "19c", "003", "127", "1ee", "1e0", "102" ], // Forward thumb.
		"1e4-" : { 7 : "152" },
		"1df" : [ "19c", "003", "12e", "1f8", "100",      ,      , "1de" ], // Unit thumb.
		"1df-" : { 7 : "15a" },
		//"1de" : [ "19c", "000", "11e", "1f7", "100" ], // dup
		// ** Index Baby Thumb
		"19c" : [ "1de", "1c4", "1ab", "19f", "19f" ],
		"19f" : [ "1f4", "1c4", "1ad", "19a", "19e" ], // index baby hinge
		"19e" : [ "1f2", "1c4", "1ad", "19a", "1a0" ], // index baby hinge, thumb out
		// "19c" : [ "1de", "1c4", "1ab", "19a", "1a0" ],
		// Index Ring Baby Thumb (All but Middle)
		"1c4" : [ "003", "19c", "14c", "001", "1ba" ],
		"1bc" : [ "003", "19c", "152", "001", "1ba" ], // Curlicue
		// ** Index middle thumb
		// Note: Consider making a shortcut to the bends.
		"11e" : [ "1ab", "18f", "124", "126", "127", "11f", "133", "12d" ], 
		"121" : [ "1ab", "18f", "1e1", "1c9", "122", "14e" ], // Index middle bent. Inaccessible except from index middle.
		"121-" : { 3 : "14e" },
		"122" : [ "1ab", "18f", "1e2", "1c9", "110", "150" ], // Index middle thumb bent. Inaccessible except via index middle.
		"122-" : { 3 : "150" },
		"136" : [ "1ab", "18f", "1e4", "128", "138" ], // Cup, index extended
		"136-" : { 3 : "102" },
		"138" : [ "1ab", "18f", "1e4", "139", "10e" ], // Cup - contact, index extended
		"138-" : { 3 : "1bb" },
		"139" : [ "1ab", "18f", "1ee", "129", "10e" ], // Cup - contact, index hinge
		"137" : [ "1ab", "18f", "128", "1c9", "13c", "13a", "13b" ], // Cup, middle extended
		"137-" : { 3 : "1d0" },
		// This thumb is wrong. ^ 10e has middle cup.
		"128" : [ "1ab", "18f", "1ed", "136", "129", "12b",      , "135" ], // Cup
		"135" : [ "1ab", "18f", "1ed", "136", "129", "12b",      , "128" ], // Cup unit, thumb forward
		"135-" : { 3 : "173" },
		"129" : [ "1ab", "191", "1eb", "138", "12a" ], // Cup contact
		// ^ Yeah, I'm kind of breaking the rules here by having the index go up.
		"129-" : { 3 : "1b1" },
		"12a" : [ "1ab", "18f", "1e6", "1c9", "118" ], // Hook
		// Not sure 1e6 is correct here.
		"13c" : [ "1ab", "191", "129", "13b", "13a" ], // Index cup contact, middle up
		"13c-" : { 3 : "1ce" },
		"13a" : [ "1ab", "191", "129", "1c9", "12a" ], // Index cup, thumb out, middle up
		"13a-" : { 3 : "1d8" },
		"13b" : [ "1ab", "191", "129", "1c9", "12a" ], // Index cup, thumb in, middle up
		"13b-" : { 3 : "1d9" },
		"126" : [ "1ab", "18f", "123", "1c9", "114",      ,      , "1db" ], // Index hinge
		"126-" : { 3 : "1db" },
		"124" : [ "1ab", "18f", "130", "123", "142",      ,      , "125" ], // Middle hinge
		"124-" : { 3 : "1c5" },
		"125" : [ "1ab", "18f", "1df", "132", "142",      ,      , "124" ], // Middle hinge, thumb unit
		"123" : [ "1ab", "18f", "1e3", "1c9", "12b",      ,      , "132" ], // Index+middle hinge
		"123-" : { 12 : "121" },
		"12b" : [ "1ab", "18f", "1ef", "1c9", "12c", "128",      , "13d" ], // Index+middle+thumb hinge
		"12b-" : { 12 : "128" },
		"12c" : [ "1ab", "18f", "1f3", "140", "112",      ,      , "13f" ], // Index+middle+thumb hinge, thumb between
		"127" : [ "1ab", "18f", "142", "137", "120",      , "12b", "134" ], // thumb forward
		"127-" : { 3 : "152", 12 : "12b" },
		"120" : [ "1ab", "18f", "1e0", "1c9", "10e",      , "122", "12f" ], // thumb bent
		"131" : [ "1ab", "18f", "143", "1c9", "116" ], // Index bent (this and two below are hooks)
		"130" : [ "1ab", "18f", "1de", "121", "117" ], // Middle bent
		"143" : [ "1ac", "18f", "121", "1c9", "116" ], // Index bent, middle hinge
		"142" : [ "1ab", "18f", "136", "141", "140",      ,      , "1c3" ], // Middle hinge contact
		"142-" : { 3 : "1c3" },
		"140" : [ "1ab", "18f", "136", "141", "113",      ,      , "1c2" ], // Middle hinge contact, thumb outside
		"140-" : { 3 : "1c2" },
		"141" : [ "1ad", "18f", "1f4", "12c", "112" ], // Middle hinge contact, thumb outside, index bent on top
		// Consider having thumb go to 11d, and back.
		"11f" : [ "1ab", "18f", "1de", "1c9", "10f", "11e" ],  // Circle
		"12d" : [ "1ab", "18e", "1de", "1c9", "134",      ,      , "12e" ], // Unit, thumb side.
		"12d-" : { 3 : "15d", 12 : "132" },
		"134" : [ "1ab", "18e", "1de", "1c9", "12f",      , "13d", "127" ], // Unit, thumb forward.
		"134-" : { 3 : "160", 12 : "13d" },
		"12f" : [ "1ab", "18e", "1e0", "1c9", "115",      ,      , "120" ], // Unit, thumb bent.
		"12f-" : { 3 : "15f" },
		"12e" : [ "1ab", "18e", "125", "1c9", "115",      ,      , "11e" ], // Unit, thumb unit.
		"12e-" : { 3 : "15a" },
		"132" : [ "1ab", "18f", "1e3", "1c9", "13d",      , "135", "123" ], // Unit hinge
		"132-" : { 3 : "180" },
		"13d" : [ "1ab", "18f", "1ef", "1c9", "13f", "135",      , "12b" ], // Unit all hinge
		"13d-" : { 3 : "17e", 12 : "135" },
		"13f" : [ "1ab", "18f", "1f4", "1c9", "13e",      ,      , "12c" ], // Unit all hinge, contact
		"13f-" : { 3 : "185" },
		"13e" : [ "1ab", "18f", "1f2", "1c9", "119", "183",      , "12c" ], // Unit all hinge, thumb outside
		"13e-" : { 3 : "183" },
		"133" : [ "1ab", "18f", "124", "126", "11a", "11f", "11e", "12d" ], // Cross
		// 13a and 13b need to fit into here somewhere in this section.
		//
		// "11e" : [ "1ab", "18f", "1de", "1c9", "10e" ],
		// ** All but Ring
		"1ab" : [ "11e", "14c", "19c", "1cb", "1a4",      , "1ac", "1ad" ],
		"1ac" : [ "143", "1b3", "19c", "1cb", "1a4",      , "1ab", "1ad" ], // Middle hinge, index thumb bent/hook
		"1ad" : [ "141", "1b3", "19f", "1cb", "1a4",      , "1ac", "1ab" ], // Middle hinge, thumb contact, index bent
		// ** All but Baby
		"18f" : [ "14c", "11e", "003", "002", "186", "191", "190", "18e" ], // New normal. "Ripple"
		"18e" : [ "15d", "12d", "003", "002", "18c", "191", "190", "18f" ], // Unit. Old normal.
		// Actually, I'm not sure if maybe 18f aught to be the default here...
		"190" : [ "14c", "11e", "003", "002", "186", "191", "18f", "18e" ], // Middle Ring bent/"ripple"
		"191" : [ "194", "129", "003", "002", "186", "18f", "190", "18e" ], // Unclear. Sort of "circle".
		// ** All fingers
		"14c" : [ "18a", "1a7", "1c5", "1db", "152", "14d", "158", "15d" ],
		"14c-" : { 7 : "1dd", 14 : "19b", 15 : "158" },
		"14d" : [ "18a", "1a7", "1c5", "1db", "152", "14c",      , "15e" ], // Heel
		"1c5" : [ "18f", "19d", "1c4", "1d7", "1c1" ], // Middle hinge 
		"1c5-" : { 3 : "1dd", 10 : "19b" },
		"1c1" : [ "18f", "1a2", "1c0", "1d7", "1c3", "1bb" ], // Middle hinge, thumb out. ("Index Ring Baby on Hinge")
		"1c1-" : { 3 : "104" },
		"1c0" : [ "18f", "1a2", "1bb", "1d7", "1bf", "1bb" ], // Middle thumb cup. ("Index Ring Baby on Cup")
		"1c0-" : { 3 : "102" },
		"1c3" : [ "18f", "1a3", "1bf", "1b3", "1c2", "1bb",      , "142" ], // Middle hinge, thumb angle contact.
		"1c3-" : { 3 : "105", 11 : "185" },
		"1c2" : [ "18f", "1a3", "1bd", "1b3", "1c5", "1bb",      , "140" ], // Middle hinge, thumb angle out.
		"1bf" : [ "18f", "1a3", "1be", "1b3", "1bd", "1bb", "1be", "1bd" ], // Middle hook
		// Note: 1bd, 1be are associated with this^.
		"1be" : [ "18f", "1a3", "1c4", "1b3", "1c5",      , "1bf", "1bd" ], // Middle hook, thumb in.
		"1bd" : [ "18f", "1a3", "1c4", "1b3", "1c5", "1c2", "1be", "1bf" ], // Middle hook, thumb out.
		"19d" : [ "1dd", "1ab", "1c4", "19b", "1a2" ], // Middle/Ring hinge. ("Baby Index Thumb on Hinge")
		"19b" : [ "180", "1ab", "1c4", "1d7", "196" ], // Index/Middle/Ring hinge. ("Baby Thumb on Hinge")
		"1a2" : [ "104", "1ab", "1c4", "1d7", "1a3" ], // Thumb/Middle/Ring hinge. ("Baby Index on Hinge")
		"1a2-" : { 9 : "17d" },
		"1a3" : [ "105", "1ab", "1c4", "196", "144" ], // Thumb/Middle/Ring hinge, w/ contact. ("Baby Index on Angle")
		"152" : [ "188", "1a6", "1c1", "1d1", "144",      , "157", "160" ], // Thumb forward.
		"152-" : { 7 : "104", 15 : "157" },
		"18a" : [ "18f", "1ab", "1c4", "1d7", "188" ], // Baby Hinge
		"18a-" : { 6 : "104", 14 : "17d" },
		"188" : [ "18f", "1ab", "1c4", "1d7", "189" ], // Baby+Thumb Hinge ("Index Middle Ring on Hinge")
		"188-" : { 6 : "104", 14 : "17d" },
		"189" : [ "18f", "1ab", "1c4", "1d7", "144" ], // w/ contact ("Index Middle Ring on Angle")
		"189-" : { 6 : "105", 14 : "185" },
		"1db" : [ "18f", "1ab", "1c4", "1d7", "1d1" ], // Index Hinge
		"1db-" : { 6 : "19b", 7 : "180" },
		"1d1" : [ "18f", "1ab", "1c4", "1d0", "1d4" ], // Index+Thumb Hinge ("Middle Ring Baby on Hinge")
		"1d1-" : { 7 : "17d" },
		"1d4" : [ "18f", "1ab", "1b3", "1d3", "1d2", "1da" ], // Index+Thumb Hinge w/ contact ("Middle Ring Baby on Angle")
		"1d4-" : { 6 : "196", 7 : "185" },
		"1d2" : [ "18f", "1ab", "1b3", "1d7", "144", "1d8", "1f2" ], // Index+Thumb Hinge, thumb out
		"1d2-" : { 7 : "183" },
		"1d3" : [ "18f", "1ab", "1b3", "1d7", "144", "1d9", "1f3" ], // Index+Thumb Hinge, thumb in
		"1d0" : [ "18f", "1ab", "1c4", "1d6", "1cf", "16d",      , "1d6" ], // Index+Thumb Cup ("Middle Ring Baby on Cup")
		"1d0-" : { 7 : "16d" },
		"1dd" : [ "18f", "1ab", "1c4", "180", "104" ], // Middle+Ring+Baby Hinge ("Index Thumb Side on Hinge")
		"104" : [ "1a2", "1ab", "1c4", "17d", "105",      ,      , "102" ], // Middle+Ring+Baby+Thumb Hinge ("Index on Hinge")
		"105" : [ "1a3", "1ab", "1c4", "185", "1dd" ], // Middle+Ring+Baby+Thumb Hinge w/ contact ("Index on Angle")
		"105-" : { 3 : "142" },
		"1a7" : [ "18f", "1ab", "19d", "1d7", "1a6" ], // Ring Hinge
		"1a7-" : { 5 : "1dd", 12 : "19b", 13 : "180"  },
		"1a6" : [ "18f", "1ab", "1a2", "1d7", "1a8", "1a5" ], // Ring+Thumb Hinge
		"1a6-" : { 5 : "104", 13 : "17d" },
		"1a8" : [ "18f", "1ab", "1a3", "1d7", "144", "1a5" ], // Ring+Thumb Hinge, w/ contact
		"1a8-" : { 5 : "105", 12 : "196", 13 : "185" },
		"196" : [ "185", "1b3", "1c4", "1d7", "19b",      , "195" ], // Index+Middle+Ring+Thumb Hinge w/ contact ("Baby Up on Angle")
		"1b3" : [ "18f", "196", "1d4", "1c3", "144",      , "1b2" ], // Index+Middle+Thumb Hinge, w/ contact. 
		"180" : [ "19b", "1ab", "1c4", "1dd", "17d", "17f", "16f", "158" ], // All Hinge, thumb side. ("Angle, No Thumb")
		// ^ associated with 182 and a billion others.
		// Maybe un-unit to spread.
		"180-" : { 3 : "132", 7 : "1e3", 15 : "16f" },
		"17d" : [ "18f", "1ab", "1c4", "104", "17e", "17b", "173", "157" ], // All Hinge Unit ("Hinge")
		"17d-" : { 3 : "13d", 15 : "173" },
		"17e" : [ "18f", "1ab", "1c4", "1d7", "185", "17c", "16d", "157" ], // All Hinge, small ("Hinge Small")
		"17e-" : { 15 : "16d" },
		"185" : [ "196", "184", "184", "105", "183" ], // All Hinge, w/ contact ("Angle")
		// Actually maybe thumb should be 180.
		// This has way too many associated things. Thumb between
		// fingers, outside fingers, spread, open, claw)
		"185-" : { 15 : "176", 31 : "176" },
		"183" : [ "18f", "1ab", "1c4", "1d7", "182" ], // All Hinge, Thumb outside Index ("Hinge, Thumb Side Touches Index")
		"183-" : { 3 : "13e", 7 : "1f2" },
		"184" : [ "18f", "1ab", "1c4", "1d7", "183" ], // All Hinge, Thumb between ring and middle
		"184-" : { 15 : "1fc" },
		"182" : [ "18f", "1ab", "1c4", "1d7", "14b", "181", "171", "159" ], // All Hinge, thumb unit ("Hinge, Thumb Side Touches Index")
		"182-" : { 15 : "171" },
		// v These don't have combos done. TODO.
		"17f" : [ "18f", "1ab", "1c4", "1d7", "17b", "180", "16e", "158" ], // All Hinge Unit, Open, thumb side
		"17b" : [ "18f", "1ab", "1c4", "1d7", "17c", "17d", "172", "155" ], // All Hinge Unit, Open, thumb forward
		"17c" : [ "18f", "1ab", "1c4", "1d7", "181", "17e", "172", "155" ], // All Hinge Unit, Open, thumb forward small
		"181" : [ "18f", "1ab", "1c4", "1d7", "14b", "182", "170", "159" ], // All Hinge Unit, Open, thumb unit
		"181-" : { 15 : "182" },
		"15d" : [ "165", "163", "163", "161", "160", "15e", "180", "15a" ], // Unit, Thumb side
		"15d-" : { 3 : "12d", 6 : "19d", 7 : "1dd", 14 : "19b", 15 : "180" },
		"15e" : [ "165", "163", "163", "161", "160", "15d",      , "15c" ], // Unit, Thumb side, Heel
		"160" : [ "165", "163", "163", "161", "15f",      , "17d", "15a" ], // Unit, Thumb forward
		"160-" : { 3 : "134", 15 : "17d" },
		"15f" : [ "165", "164", "164", "161", "15a",      ,      , "15a" ], // Unit, Thumb bent
		"15f-" : { 3 : "12f", 7 : "1e0", 15 : "1f9" },
		"161" : [ "165", "163", "163", "1db", "160",      ,      , "15a" ], // Unit, Thumb side, split index
		"165" : [ "18e", "162", "162", "161", "160",      , "196", "14c" ], // Unit, thumb unit, Split baby
		"163" : [ "18f", "1ab", "15d", "1d7", "164",      ,      , "162" ], // Kohen, thumb side
		"164" : [ "18f", "1ab", "15f", "1d7", "148",      ,      , "162" ], // Kohen, thumb bent
		"162" : [ "18f", "165", "15a", "1d7", "163",      ,      , "163" ], // Kohen, thumb unit
		"15a" : [ "165", "162", "162", "161", "147", "15c", "182", "14c" ], // Unit, Thumb unit
		"15a-" : { 3 : "12e", 7 : "1df", 15 : "181" },
		"15c" : [ "165", "162", "162", "161", "147", "15a",      , "14d" ], // Unit, Thumb unit, Heel
		// ^ This is a bit of a mess. Some of the things don't exist with thumbs.
		"158" : [ "18f", "1ab", "1c4", "1d7", "157",      , "153", "180" ], // Spread hinge, thumb side
		// ^ This has complicated unit forms. Unit hinge with spread thumb and spread hinge with
		// unit thumb are both things that exist. Going with unit hinge for now.
		"158-" : { 3 : "123", 15 : "14e" },
		"157" : [ "18f", "1ab", "1c4", "1d7", "159", "155", "153", "17d" ], // Spread hinge, thumb forward
		"157-" : { 3 : "12b", 7 : "1ef", 15 : "154" },
		"159" : [ "18f", "1ab", "1c4", "1d7", "146",      , "153", "182" ], // Spread hinge, thumb unit
		"155" : [ "18f", "1ab", "1c4", "1d7", "159", "157", "154", "17b" ], // Spread hinge, Open
		"153" : [ "18f", "1ab", "1c4", "1d7", "157", "154", "156", "16d" ], // Spread cup, (thumb forward, sort of)
		"154" : [ "18f", "1ab", "1c4", "1d7", "157", "153", "156", "16c" ], // Spread cup open, (thumb forward, sort of)
		"156" : [ "18f", "1ab", "1c4", "1d7", "157",      , "14e", "17a" ], // Spread oval, (thumb forward, sort of)
		// We're not counting the thumb forwards simply for ease of access. 
		"14e" : [ "190", "1ab", "1c4", "1d7", "150", "14f", "14c", "167" ], // All bent but thumb. 
		"14e-" : { 3 : "121", 7 : "1e1" },
		"14f" : [ "18f", "1ab", "1c4", "1d7", "151", "14e", "14d" ], // All bent but thumb, Heel.
		"150" : [ "18f", "1ab", "1c4", "1d7", "145", "151", "14c", "166" ], // All bent. 
		"150-" : { 3 : "122", 7 : "1e2" },
		"151" : [ "18f", "1ab", "1c4", "1d7", "145", "150", "14d" ], // All bent, Heel.
		"16d" : [ "165", "163", "163", "102", "173", "16c", "17a", "153" ], // Cup Unit
		"16d-" : { 7: "1ed", 15 : "17a" },
		"16c" : [ "165", "163", "163", "161", "172", "16d", "17a", "154" ], // Open Cup Unit
		"173" : [ "165", "163", "163", "161", "16f", "172", "17a", "153" ], // Cup Unit forward
		"173-" : { 15 : "17a" },
		"172" : [ "165", "163", "163", "161", "16e", "173", "17a", "154" ], // Open Cup Unit forward
		"16f" : [ "165", "163", "163", "161", "171", "16e", "178", "153" ], // Cup Unit, thumb side
		"16f-" : { 15 : "178" },
		"16e" : [ "165", "163", "163", "161", "170", "16f", "178", "154" ], // Open Cup Unit, thumb side
		"171" : [ "165", "162", "162", "161", "16d", "170", "179", "153" ], // Cup Unit no thumb
		"171-" : { 15 : "179" },
		"170" : [ "165", "162", "162", "161", "16c", "171", "179", "154" ], // Open Cup Unit no thumb
		"102" : [ "165", "163", "11f", "16d", "101", "1e4", "103", "104" ], // Cup Unit, Index up
		"17a" : [ "165", "163", "163", "161", "177", "16c", "169", "156" ], // Oval Unit
		"17a-" : { 15 : "169" },
		"177" : [ "195", "163", "163", "103", "178", "16c", "16b", "156" ], // Oval Unit, contact
		"177-" : { 3 : "1b2" },
		"195" : [ "177", "1b2", "163", "161", "178", "16c", "196", "156" ], // Oval Unit, contact, baby up
		"1b2" : [ "177", "195", "163", "161", "178", "16c", "1b3", "156" ], // Oval Unit, contact, baby ring up
		// these two should have different alts, and be associated with baby and baby ring, respectively.
		"103" : [ "195", "163", "163", "177", "178", "16c", "101", "156" ], // Oval Unit, contact, index up
		// I have no idea where to go with Baby here.
		"178" : [ "165", "163", "163", "161", "179", "16e", "167", "156" ], // Oval Unit, thumb side
		"178-" : { 15 : "167" },
		"179" : [ "165", "162", "162", "161", "17a", "170", "168", "156" ], // Oval Unit, no thumb
		"179-" : { 15 : "168" },
		"166" : [ "165", "163", "163", "1e9", "169",      , "160", "150" ], // Claw Unit
		"169" : [ "165", "163", "163", "1e9", "16b",      , "160", "14e" ], // Claw Unit, thumb forward
		"169-" : {  },
		"16b" : [ "165", "163", "163", "1e9", "16a",      , "160", "14e" ], // Claw Unit, hook
		"16a" : [ "165", "163", "163", "1e9", "149",      , "160", "14e" ], // Claw Unit, curlicue
		"167" : [ "165", "163", "163", "1e9", "168",      , "15d", "14e" ], // Claw Unit, thumb side
		"167-" : { 3 : "121", 7 : "1e1", 15 : "1f7" },
		"168" : [ "165", "163", "163", "1e9", "166",      , "15a", "14e" ], // Claw Unit, no thumb
		"168-" : { 15 : "1f8" },
		"1e9" : [ "165", "163", "184", "16b", "166", "1d3", "1d9", "1e8" ], // Claw Unit, index cup, thumb inside
		
		// "14c" : [ "18f", "1ab", "1c4", "1d7", "144" ],
		
		// FAKES:
		"001" : [ "1b8", "19a", "1d7", "1c4", "1b0" ], // Baby Ring Thumb
		"002" : [ "1d7", "1c9", "1b8", "18f", "1b4" ], // Ring Middle Thumb
		"003" : [ "1c4", "1de", "18f", "1b8", "1b7" ], // Ring Index Thumb
		
		// ...
		//"100" : [ "192", "1ae", "1c6", "100", "1f7" ],
		//"100" : [ "192", "1ae", "1c6", "100", "1f7" ],
		"x" : [ "192", "1ae", "1c6", "100", "1f7" ]
	},
	
	/*
		Plane							Surface
		Diagonal, Finger, BothHands, Contact, 
		Hand/Wrist, Curve, Multiple, Enlarge,
	// Still needs something for alternate, though.
	// TODO: Headless arrows. I might just use the ? for this.
	// Incomplete.
	*/
	movementSymbols = {
		"default" : "H7",
		"H1" : "alt7",
		"H2" : { // Finger movements
			"default": "H2",
			"H1" : { // Flick
				"default": [[[
					[ 
						[[ "21b", "2200R8" ]], 
						"21f0R8F", "21f1R8F", "21f2R8F", "21f3R8F" 
					], 
					"21c"
				]]],
				"H1" : {
					"default" : [[ "21d0R4", "21e0R4" ]],
					"H1" : {
						"default": [ "21d1R4", "21e1R4" ],
						"H1" : "alt2"
					}
				}
			},
			"H2" : { // Squeeze
				"default": [[[
					[
						[[ "216", "2201R8"]], 
						"21a0R8F", "21a1R8F", "21a2R8F", "21a3R8F"
					], 
					"217"
				]]],
				"H2" : {
					"default": [["2180R4", "2190R4"]],
					"H2" :  {
						"default": ["2181R4", "2191R4"],
						"H2" : "alt2"
					}
				}
			}, 
			"H3" : "alt6", // "Sequential"
			"H4" : {
				// Finger circle arrows, from a different section.
				sortBy: "plane",
				0: {
					"default": "2f10R4F",
					"H4" : "2f20R4F"
				},
				1: { // This one's rotation is complicated. TODO.
					"default": "2f30R8",
					"H4" : "2f40R8"
				}
			},
			"H5" : { // Finger movement arrows.
				// TODO: Needs flipped versions.
				sortBy: "plane",
				0: {
					"default": [ "2280R8" ],
					"H5" : {
						"default": "2281R8",
						"H5" : {
							"default": "2283R8",
							"H5" : "alt2"
						}
					}
				},
				1: {
					"default": [ "2290R8" ],
					"H5" : {
						"default": "2291R8",
						"H5" : {
							"default": "2293R8",
							"H5" : "alt2"
						}
					}
				}
			},
			"H6" : { // Hinge movement
				"default": [[[ 
					[ // Small
						[["2210R8", "2211R8", "2212R8", "2213R8", "2214R8"]],
						// Hinge sequential. Downwards is still missing. TODO.
						"2230R8", "2231R8", "2232R8", "2233R8" 
					], 
					[ // Large
						[["2220R8", "2221R8", "2222R8", "2223R8", "2224R8" ]]
					]
				]]],
				"H6" : { // alternating
					"default": [[ 
						[[["2250R8", "2251R8", "2252R8", "2253R8" ]]],
						[[["2260R8", "2261R8", "2262R8", "2263R8" ]]]
					]]
				},
				"H5" : { // Scissors.
					"default": [[[[[ "2270R8", "2271R8", "2272R8", "2273R8" ]]]]]
				}
			},
			"H7" : "alt8",
			"HO" : generalActions.faceToggle
		},
		"H3" : generalActions.toggleBothHands,
		"H4" : ( function() { // All contact symbols.
			var x = { 
					"default": "H4",
					"H6" : function(){} // blank spot so that we don't get 
					// random zigzags from the other section showing up.
					// Maybe I'll fill it with something real later.
				},
				contactTypes = {
					"H1" : "20e", "H2": "20b", "H3" : "208", "H4" : "205", 
					"H8" : "211"
				}, i, sub, subsub, mult, surf;
			for ( i in contactTypes ) {
				mult = ( parseInt( contactTypes[ i ], 16 ) + 1 ).toString( 16 );
				surf = ( parseInt( contactTypes[ i ], 16 ) + 2 ).toString( 16 );
				sub = x[ i ] = {};
				sub[ "default" ] = [[ contactTypes[ i ], surf + "0R4" ]];
				( subsub = sub[ i ] = {} )[ "default" ] = 
					[ mult + "0R4", surf + "1R4" ];
				( subsub[ i ] = {} )[ "default" ] = mult + "1R4";
				subsub[ i ][ i ] = "alt2";
			}
			return x;
		} )(),
		"H5" : { // Hand/Wrist
			"default": {
				sortBy: "plane",
				0: {
					"default": [[ "22eRBHR8" ]],
					"H5" : { // Double
						"default": { 
							sortBy: "face",
							0: [ "230RBHR8" ], // Regular
							2: [ "230RBHR8" ],
							1: [ "232RBHR8" ], // Alternating
							3: [ "232RBHR8F" ] // Alternating, reversed.
						},
						"H5" : {
							"default": { 
								sortBy: "face",
								0: "235RBHR8", // Regular
								2: "235RBHR8",
								1: "237RBHR8", // Alternating
								3: "237RBHR8F" // Alternating, reversed.
							},
							"H5" : "alt2"
						}
					},
					"H6" : { // Wrist circles
						"default": "2edRBHR8F",
						"H6" : "2eeRBHR8F" // Double
					}
				},
				1: {
					"default": [[ "269RBHR8" ]],
					"H5" : { // Double
						"default": { 
							sortBy: "face",
							0: [ "26bRBHR8" ], // Regular
							2: [ "26bRBHR8" ],
							1: [ "26dRBHR8" ], // Alternating
							3: [ "26dRBHR8F" ] // Alternating, reversed.
						},
						"H5" : {
							"default": { 
								sortBy: "face",
								0: "270RBHR8", // Regular
								2: "270RBHR8",
								1: "272RBHR8", // Alternating
								3: "272RBHR8F" // Alternating, reversed.
							},
							"H5" : "alt2"
						}
					},
					"H6" : { // These have six rotations, in no consistent 
							 // order. TODO: Something.
						"default": "2efRBHR8",
						"H6" : "2f0RBHR8"
					}
				}
			},
			"HO" : generalActions.faceLoop
		},
		"H6" : { // Curved arrows
			/*
				
				Still need:  
				Curve Straight, ArmSpiral, Curved Cross...
				Still not done as of 2015-03-25. TODO.
				
				Keyboard map:
				Hit Plane, Hump/Loop, BH, Bend/Corner/Check/Box
				Rotate X Plane, More degrees, Wave, Enlarge
				Bend + Bend = Zigzag, +Bend = Peaks
				
				Hump, Loop, Wave, Big Wave, Double Loop
				
				RotateX can have a lot of subs, whatever.
				Possibilities: TravelRotation, CornerWallPlaneWithRotation, 
				ArmSpiral, ...
				
				CurveCross and ArmSpiral are wall-exclusive.
				RotateX has Multiple as a sub-option.
				The "circles" section is much bigger than I would expect.
				Arm circles, Wrist circles, Finger circles...
			*/
			sortBy: "plane",
			0: { // Wall Plane
				"default": [[[[
						[ ["288RBHR8F", "2a6RBHR4", "2adRBHR4"], "28cRBHR8F", "290RBHR8F", "2e3RBHR8F","2e5RBHR8F" ],
						[ "289RBHR8F", "28dRBHR8F", "291RBHR8F", "2e4RBHR8F","2e6RBHR8F" ],
						[ "28aRBHR8F", "28eRBHR8F" ],
						[ "28bRBHR8F", "28fRBHR8F" ]
				]]]],
				// "H1" : "2a6RBHR4", // Hits X Plane
				"H2" : {  // Hump, Loop, Double Loop
					"default": [[[[["292RBHR8F","2a7RBHR4","2aeRBHR4"]], "293RBHR8F", "294RBHR8F" ]]],
					"H2" : {
						// Loop
						"default": [[ [["295RBHR8F", "2a8RBHR4", "2afRBHR4"]], "296RBHR8F", "297RBHR8F" ]], 
						"H2" : { // Double loop.
							"default": "298RBHR8F", 
							"H2" : "alt1"
						}
					}
				},
				"H4" : { // Bend, Corner, Check, Box, Zigzag, Peaks
					"default": [[[ // Bend    Corner       Check        Box
							[ "238RBHR8F", "23bRBHR8F", "23fRBHR8F", "242RBHR8F" ], 
							[ "239RBHR8F", "23cRBHR8F", "240RBHR8F", "243RBHR8F" ], 
							[ "23aRBHR8F", "23dRBHR8F", "241RBHR8F", "244RBHR8F" ]
					]]],
					"H4" : { // Zigzag
						"default": [[ "245RBHR8F", "246RBHR8F", "247RBHR8F" ]],
						"H4" : { // Peaks
							"default": ["248RBHR8F", "249RBHR8F", "24aRBHR8F"],
							"H4" : "alt1"
						}
					},
					"H5" : "23eRBHR8F" // Corner Wall Plane with Rotation.
					// Doesn't occur in floor plane.
					// This should probably be duplicated in at least one other
					// spot. It might be hard to find, otherwise.
				},
				"H5" : { // Rotate X Plane
					"default": "H5",
					// Lots of stuff can be put here, I think.
					// "Travel rotation" is complicated, as there's three plane
					// combinations. 
					"H2" : "alt8",
					"H5" : {
						"default" : [[[[ "2a2RBHR8F", "2aaRBHR4", "2b1RBHR4" ]]]],
						"H5" : { // Double rotations.
							"default": [[[
								["2a3RBHR8F","2a4RBHR8F"], 
								["2abRBHR4", "2acRBHR4"], 
								["2b2RBHR4", "2b3RBHR4"]
							]]],
							"H5" : "2a5RBHR8F"
						}
					},
					"H6" : { // Travel Rotation Single Wall Plane
						"default" : "284RBFR8F",
						"H6" : { // double arrow
							"default": [[[[ "285RBFR8F", "286RBFR8F" ]]]],
							"H6" : "287RBHR8" // shaking
						}
					},
					"H7" : { // Travel Rotation Single Wall Plane
						"default": "24bRBFR8F",
						"H7" : { // double arrow
							"default": [[[[ "24cRBFR8F", "24dRBFR8F" ]]]],
							"H7" : "251RBHR8" // shaking
						}
					}
				},
				"H6" : "alt6", // More degrees
				"H7" : { // Wave
					"default": [[[ 
						[
							[ "299RBHR8F", "2a9RBHR4", "2b0RBHR4" ], 
							[ "29cRBHR8F", "2b4RBHR8F" ]
						],
						[ "29aRBHR8F", ["29dRBHR8F", "2b5RBHR8F"] ],
						[ "29bRBHR8F", ["29eRBHR8F", "2b6RBHR8F"] ]
					]]]
				},
				"HO" : generalActions.faceLoop
			},
			1: { // Floor plane
				"default": [[[[ // Need to fit Curved Floor Combined in here.
								// Rotations look absurdly complicated. TODO.
								// The loops need an extra rotate direction...
					[ 
						[ "2d5RBHR8F", "2b7RBHR8", "2c6RBHR8" ], 
						/*
						{
							sortBy: "rotation",
							0: "2d9RBH0",
							1: "2d9RBH3",
							2: "2d9RBH2",
							3: "2d9RBH4",
							4: "2d9RBH6",
							5: "2d9RBH7",
							6: "2d9RBH5",
							7: "2d9RBH1",
						},
						*/
						"2e7RBFR8F", 
						"2eaRBFR8F" 
					],
					[ [ "2d6RBHR8F", "2b8RBHR8", "2c7RBHR8" ], "2e8RBFR8F", "2ebRBFR8F" ],
					[ "2d7RBHR8F", "2e9RBFR8F", "2ecRBFR8F" ],
					[ "2d8RBHR8F" ]
				]]]],
				"H2" : { // TODO: Hits X Double humps, double loops
					"default": [[[ // Humps
							[[ "2daRBHR8F", "2b9RBHR8", "2c8RBHR8" ]], // small
							[[ "2daRBHR8F", "2baRBHR8", "2c9RBHR8" ]] // large
						]]],
					"H2" : [[[ // Loops
						[[ "2dbRBHR8F", "2bdRBHR8", "2ccRBHR8" ]], // small
						[[ "2dbRBHR8F", "2beRBHR8", "2cdRBHR8" ]] // large
					]]]
				},
				"H4" : {
					"default": [[[
						[ "273RBHR8F", "274RBHR8F", "277RBHR8F", "278RBHR8F" ],
						[ "273RBHR8F", "275RBHR8F", "277RBHR8F", "279RBHR8F" ],
						[ "273RBHR8F", "276RBHR8F", "277RBHR8F", "27aRBHR8F" ]
					]]],
					"H4" : {
						"default": [[ "27bRBHR8F", "27cRBHR8F", "27dRBHR8F" ]],
						"H4" : {
							"default": [ "27eRBHR8F", "27fRBHR8F", "280RBHR8F" ],
							"H4" : "alt1"
						}
					}
				},
				"H5" : { // Rotate Y Plane
					"default": "H5",
					"H2" : "alt8",
					"H5" : {
						"default": [[[["2dfRBHR8F", "2c3RBHR8", "2d2RBHR8"]]]],
						"H5" : {
							"default": [[[
								["2e0RBHR8F", "2e1RBHR8F"], 
								["2c4RBHR8", "2c5RBHR8"], 
								["2d3RBHR8", "2d4RBHR8"]
							]]],
							"H5" : "2e2RBHR8F"
						}
					},
					"H6" : { // Travel Rotation Single Wall Plane
						"default" : "24eRBFR8F",
						"H6" : { // double arrow
							"default": [[[[ "24fRBFR8F", "250RBFR8F" ]]]],
							"H6" : "251RBHR8" // shaking
						}
					},
					"H7" : { // Travel Rotation Single Wall Plane
						"default": "281RBFR8F",
						"H7" : { // double arrow
							"default": [[[[ "282RBFR8F", "283RBFR8F" ]]]],
							"H7" : "287RBHR8" // shaking
						}
					}
				},
				"H6" : "alt6",
				"H7" : {
					"default": [[[
							[[ "2dcRBHR8F", "2c1RBHR8", "2d0RBHR8" ]], 
							[[ "2ddRBHR8F", "2c2RBHR8", "2d1RBHR8" ]], 
							[[ "2deRBHR8F", "2c2RBHR8", "2d1RBHR8" ]]
					]]]
				},
				"HO" : generalActions.faceLoop
			}
		},
		"H7": { // Regular arrows, with multiples
			sortBy: "plane",
			0: {
				/*
				"default": [[[
						[
							[[ "22aRBHR8", {
								sortBy: "rotation",
								default: "255RBHR8",
								2: "265RBH1",
								6: "265RBH7"
							}, "259RBHR8" ]], 
							[[ "22bRBHR8", "256RBHR8", "25aRBHR8" ]], 
							[[ "22cRBHR8", "257RBHR8", "25bRBHR8" ]], 
							[[ "22dRBHR8", "258RBHR8", "25cRBHR8" ]]
						],
						"2141R8", // Surface symbols...
						"2151R4"
				]]],
				*/
				"default": [[[
						( function () {
							for ( var x = [], i = 0, y; i < 4; i++ ) {
								x.push( [ y = [ 
									// Regular arrows
									"22" + ( 10 + i ).toString( 16 ) + "RBHR8"
								] ] );
								for ( var ii = 0; ii < 8; ii += 4 ) {
									y.push( { 
										sortBy: "rotation",
										// Normal diagonal arrows.
										"default": "25" + ( 5 + i + ii ).toString( 16 ) + "RBHR8",
										// Use equivalents from the floor plane.
										2: "26" + ( 5 + i ).toString( 16 ) + "RBH" + ( ii ? 3 : 1 ),
										6: "26" + ( 5 + i ).toString( 16 ) + "RBH" + ( ii ? 5 : 7 )
									} );
								}
							}
							return x;
						})(),
						"2141R8", // Surface symbols...
						"2151R4"
				]]],
				
				"H7" : { // Doubles.
					"default": { 
						sortBy: "face",
						0: [[ "22fRBHR8" ]], // Regular
						2: [[ "22fRBHR8" ]],
						1: [[ "231RBHR8" ]], // Alternating
						3: [[ "231RBHR8F" ]] // Alternating, reversed.
					},
					"H6" : "233RBHR8F", // Cross Movement, X Plane
					"H7" : { // Triples
						"default" : { 
							sortBy: "face",
							0: [ "234RBHR8" ], // Regular
							2: [ "234RBHR8" ],
							1: [ "236RBHR8" ], // Alternating
							3: [ "236RBHR8F" ] // Alternating, "reversed". (-.-)
						},
						"H7" : "alt1"
					},
					"HO" : generalActions.faceLoop
				}
			},
			1: { 
				"default": [[[
					["265RBHR8", "266RBHR8", "267RBHR8", "268RBHR8"],
					"2140R8",
					"2150R4"
				]]],
				"H7" : { // Doubles
					"default": { 
						sortBy: "face",
						0: [[ "26aRBHR8" ]], // Regular
						2: [[ "26aRBHR8" ]],
						1: [[ "26cRBHR8" ]], // Alternating
						3: [[ "26cRBHR8F" ]] // Alternating, reversed.
					},
					"H6" : "26eRBHR8F", // Cross Movement, X Plane
					"H7" : { // Triples
						"default" : { 
							sortBy: "face",
							0: [ "26fRBHR8" ], // Regular
							2: [ "26fRBHR8" ],
							1: [ "271RBHR8" ], // Alternating
							3: [ "271RBHR8F" ] // Alternating, "reversed". (-.-)
						},
						"H7" : "alt1"
					},
					"HO" : generalActions.faceLoop
				}
			}
		},
		"H8" : "alt5", // Enlarged arrows
		"HO" : "alt4", // Surface and between contacts. 
		"HL" : generalActions.rotateLeft, 
		"HR" : generalActions.rotateRight,
		"HP" : generalActions.togglePlane
	},
	
	// This shouldn't switch back to default when something's clicked twice.
	// TODO.
	dynamicsSymbols = {
		"default": "H4",
		"H1" : {
			"default": "2faRH0",
			"H1" : {
				sortBy: "rightHand",
				"false": "2fa30",
				"true": "2fa20"
			}
		},
		"H2" : {
			"default": "2f9RH0",
			"H2" : {
				sortBy: "rightHand",
				"false": "2f930",
				"true": "2f920"
			}
		},
		"H3" : "2fb0R8",
		"H4" : {
			"default": "2f7RH0",
			"H4" : {
				sortBy: "rightHand",
				"false": "2f730",
				"true": "2f720"
			}
		},
		"H5" : "2fc0R8",
		"H6" : "2fe0R8",
		"H7" : "2fd0R8",
		"H8" : "2f80R8",
		"HL" : generalActions.rotateLeft,
		"HR" : generalActions.rotateRight
	},
	
	// There should probably be a third method for simply "doubling"...
	// Incomplete.
	headSymbols = {
		// NOTE: Presumably, the user will arrive here by using their middle
		// finger, or sometimes the ring finger. As such, H3 and H2 are among
		// the hardest sections to reach. 
		"default": { // NOTE: If, at any point, I add the "headless" equiv of
					 // the various shapes, the headless version of base is the
					 // clipped-top one.
			sortBy: "plane", 
			0: "2ff0R4",
			1: "2ff2R4"
		},
		"H1" : { // cheeks, ears, nose, breath, "excitement" (?)
			// Basically everything in the "middle" area between mouth and eyes.
			// This has the Air Blowing Out/Sucking In heads, but not the 
			// rotations. Hmmm..
			// This is badly cramped. Consider splitting.
			"default": "H4",
			"H1" : "335RBE",
			"H2" : "336RBE",
			"H3" : {
				"default" : "32eRBE",
				"H7" : "32dRBE",
				"H8" : "32fRBE"
			},
			"H4" : {
				"default" : "32bRBE",
				"H7" : "32aRBE",
				"H8" : "32cRBE"
			},
			"H5" : "330RBE",
			"H6" : {
				"default" : "331",
				"H6" : "332",
				"H7" : "333",
				"H8" : "334"
			},
			"H7" : function () {},
			"H8" : function () {},
			"HO" : generalActions.toggleBothHands,
			"HP" : "36c", // Excitement
			// These flip for righthand. TODO.
			// These are also missing the two smaller sizes.
			"HL" : "339", // Exhale
			"HR" : "33a"  // Inhale
		},
		"H2" : { // Not sure what to put here. Empty for now.
			// Remaining heads: Some unidentified head position 
			// things, including: The various 307s, 2ff1 (maybe a plane rotation
			// of the default?), 2ff3 (?), maybe more. Haven't looked enough. 
			// (I'll also probably need to cram one mouth somewhere.)
			// (And maybe an eyes or two.)
			// Maybe find out which head positioning things are most
			// "neck"-related, and put those in a section? 
			// Another option is to make this section all "contact" stuff.
			// That includes: Nose contact (S332), mouth contact (S33d) (yay, 
			// found the mouth), forehead contact (S312), head rims (S3000R8) 
			// (requires new default for the section), ... Hm, that's not 
			// enough.
			// Checking how contact section would look. Hm, not  enough symbols.
			/*
			"default" : "3000R8",
			"H1" : "33d",
			"H2" : "332",
			"H3" : "312"
			*/
		},
		"H3" : { // Eyebrows, forehead, hair.
			"default" : "H3",
			"H1" : "311",
			"H2" : "30bRBE",
			"H3" : "30aRBE", // default raised eyebrows
			"H4" : "30cRBE",
			"H5" : "30eRBE",
			"H6" : "30fRBE",
			"H7" : "310RBE",
			"H8" : "30dRBE",
			"HP" : "313",
			"HL" : "312", // Forehead contact.
			"HR" : "36b", // Hair
			"HO" : generalActions.toggleBothHands
		}, 
		"H4" : { // Eyes. Currently incomplete. 
			// Still missing Blink (single: 317, multiple: 318), some Eyelashes,
			// and a bunch of Gazes.
			"default": [
				"314RBE" 
			],
			"H1" : "315RBE",
			"H2" : "316RBE",
			"H3" : "319RBE",
			"H4" : { // gazes
				// Consider remerging one level up. We might have enough room.
				// Note: R8 = 8 rotations, R8F = 8 Rs + flip, R4F = 4 R's + flip
				// Split 1/2 loop and 3/4 into their own keys if this keeps its
				// own section.
				// MISSING: Doubles (322, 325), Alternating (323, some of 326),
				// Bent (other 326, floor only). 
				sortBy: "plane",
				// Many of these are dups. Shouldn't be necessary.
				0 : {
					"default": [ "3210R8", "3270R8F", "3290R4F"],
					"HP" : generalActions.togglePlane,
					"H4" : "alt3",
					"HL" : generalActions.rotateLeft,
					"HR" : generalActions.rotateRight,
					"HO" : generalActions.faceToggle
				},
				1 : {
					"default": [ "3240R8", "3280R4F" ],
					"HP" : generalActions.togglePlane,
					"H4" : "alt3",
					"HL" : generalActions.rotateLeft,
					"HR" : generalActions.rotateRight,
					"HO" : generalActions.faceToggle
				}
			},
			"H5" : "31aRBE",
			"H6" : "31bRBE",
			"H7" : "31cRBE",
			"H8" : "31dRBE",
			"HP" : { // Eyelashes. 
				"default": ["31eRBE"],
				"HP" : {
					"default": "31fRBE",
					"HP" : {
						"default": "320RBE",
						"HP" : "alt2"
					}
				}
			},
			"HO" : generalActions.toggleBothHands
		},
		"H5" : { // Teeth, jaw, neck. 
			"default": ["361", "3620", "3621", "3622" ],
			"H1" : "alt2=1",
			"H2" : "alt2=2",
			"H3" : "alt2=3",
			"H4" : {
				sortBy: "plane",
				0: "3680R8",
				1: "3690R8"
			},
			"H5" : [ "363R2", "3640", "3641", "3642" ],
			"H6" : [ "365R2", "3660", "3661", "3662" ],
			"H7" : "367RBE",
			"H8" : "36a",
			"HO" : generalActions.toggleBothHands,
			// If HP is added to the main section, remove this.
			"HP" : generalActions.togglePlane
		},
		"H6" : { // Tongue
			// TODO: Consider merging some of these.
			// INCOMPLETE, and apparently no room...
			"default": "3590R8",
			"H1" : "35a0R8",
			"H2" : "35b0R8",
			"H3" : "35c0R8",
			"H4" : "35d0R8",
			"H5" : "35f00",
			"H6" : "35f10",
			"H7" : "36000",
			"H8" : "36010",
			"HO" : "35e0R8", // irregular. needs flipping.
			"HP" : "35f20"
		}, 
		"H7" : { // Mouth stuff
			// These were placed pretty much at random.
			// First layer is wrinkled, second is open, third is movement,
			// fourth lips and such.
			// Not enough buttons. Some of these will need to be merged.
			// Still missing: One-side. 
			// HO is occupied, contents need to be moved.
			// The lips merge is probably not the way it should be...
			// Argh, too cramped for space...
			"default": [
				[  // unwrinkled
					[ // closed
						["33b", "353", "354", "355"], // normal, various lips
						"33c" // movement
					],
					["344","345"], // open
					"349" // yawn
				],
				["357RBE0","346"], // wrinkled
				"358RBE0" // double-wrinkled
			],
			"H1" : "33d", // contact
			"H2" : "alt4", // movement
			"H3" : "alt3", // opening
			"H4" : [["33e", "340"], "33f"], // smile
			"H5" : [["34a", "34c"], "34b"], // rectangle
			"H6" : [[["34d", "34e"]],"34f"], // kiss
			"H7" : "alt2", // wrinkle
			"H8" : [["341", "343"], "342"], // frown
			"HP" : [[["350", "351","352"]]], // tense
			"HO" : ["347", "348"], // oval
			"HL" : "alt5", // lips and stuff
			// TODO: Add one-sided corner press. Depends on rightHand.
			// also, one-sided wrinkle and one-sided double wrinkle.
			"HR" : "356RBE0" // pressed corners
		},
		"H8" : { // general head movement
			// I dislike some of these being split by rightHand.
			// Some of these will probably be split. Symbols that no longer fit
			// can go in the spare area.
			// Maybe split into "head movement" and "head position/view".
			"default": "3000R8",
			"H1" : "3090R8H",
			"H2" : "3080R8",
			"H3" : {
				sortBy: "plane",
				0: "3040R2F",
				1: "3050R2F"
			},
			"H4" : { // TODO: Fix face mess, and H4*5 bug.
				sortBy: "plane",
				0: {
					"default": "3010R8",
					"H4" : {
						"default": {
							sortBy: "face",
							0: "3011R8",
							1: "3012R4",
							2: "3011R8",
							3: "3012R4F"
						},
						"H4": {
							"default": {
								sortBy: "face",
								0: "3013R8",
								1: "3014R8",
								2: "3013R8",
								3: "3014R8"
							}
							//"H4": "alt3"
						}
					},
					"HO" : generalActions.faceLoop
				}, 
				1: {
					"default": "3030R8",
					"H4" : {
						"default": {
							sortBy: "face", 
							0: "3031R8",
							1: "3032R4",
							2: "3031R8",
							3: "3032R4F"
						},
						"H4" : {
							"default": {
								sortBy: "face",
								0: "3033R8",
								1: "3034R8",
								2: "3033R8",
								3: "3034R8"
							}
							//"H4": "alt3"
						}
					},
					"HO" : generalActions.faceLoop
				}
			},
			"H5" : { 
				"default": ["3020RH"],
				"H5" : {
					"default": "3021RH",
					"H5" : {
						"default": "3022RH",
						"H5" : "alt2"
					}
				}
			},
			// Consider setting up RF for these
			"H6" : {
				sortBy: "face",
				0: {
					"default": "30230",
					"H6" : "30240"
				},
				1: {
					"default": "30231",
					"H6" : "30241"
				}
			},
			"H7" : {
				"default": {
					"sortBy" : "face",
					0: "30600",
					1: "30601"
				},
				"H7" : {
					"sortBy" : "face",
					0: "30602",
					1: "30603"
				}
			},
			"H8" : "",
			"HP" : generalActions.togglePlane,
			"HO" : generalActions.faceToggle
			
		}, 
		// Consider leaving these four unused (or used for original purposes).
		"HP" : generalActions.togglePlane, 
		"HO" : generalActions.toggleBothHands, 
		// "HL":{},"HR":{},
		"HL" : generalActions.rotateLeft, 
		"HR" : generalActions.rotateRight
	},
	
	// Are finger symbols necessary? I don't know.
	// Incomplete.
	bodySymbols = {
		"default": "36d0R4",
		"H1" : { // "Shoulder Hip Move"
			// Rotating is backwards for rightHand. 
			sortBy: "plane",
			0: {
				"default": ["36f0R8H"],
				"H1" : {
					"default": [["36f1R8H", "36f2R8H"]],
					"H1" : {
						"default": ["36f3R8H", "36f4R8H"],
						"H1" : "alt1"
					}
				}
			},
			1: {
				"default": [[[ "3700R8H", "3700R8H" ]]],
				"H1" : {
					"default": [["3701R8H", "3702R8H"]],
					"H1" : {
						"default": ["3703R8H", "3704R8H"],
						"H1" : "alt1"
					}
				}
			}
		},  
		"H2" : { // "Shoulder Tilts"
			// Also TODO: Alternating tilts.
			"default": [[[ "3710R8H", "3710R8H" ]]],
			//default: [ "3710R8H", [[["3711R4", "3712R4"]]], [[["3713R4", "3714R4"]]]],
			"H2" : {
				"default": [["3711R8H", "3712R8H"]],
				"H2" : {
					"default": ["3713R8H", "3714R8H"],
					"H2" : "alt1"
				}
			}
			//"H2" : "alt2"
		},  
		"H3" : { // "Torso Straight Stretch"
			// Rotations are inconsistent. Hmmm...
			// Incomplete.
			sortBy: "rotation",
			"0": "37200",
			"1": "37201",
			"2": "37210", // Sideways. Can't find an image for this.
			"3": "37213",
			"4": "37202",
			"5": "37211",
			"6": "37212", // Same.
			"7": "37203"
		},  
		"H4" : { // limbs
			// These are a mess.
			// This needs to be reworked. TODO.
			// The right-handed-ness might actually be ambiguous. Hard to tell.
			// Maybe there should be a key to lock the symbol to a nearby
			// body symbol? Would need an icon, and a way to pass icons to the 
			// keyboard functions.
			"default": [ 
				["37a0R8H", "3790R8H", "3780R8H", "3770R8H", "37b0R8H", "37c0R8H", "37d0R8H"], 
				["37a1R8H", "3791R8H", "3781R8H", "3771R8H", "37b1R8H", "37c1R8H", "37d1R8H"] 
			], 
			"H4" : "alt2",
			"H3" : "alt3",
			// This doesn't make any sense at all, because I don't understand the symbols.
			// Need to ask someone about this.
			"H2" : "3760R8H"
		},
		"H5" : { // "Torso Curved Bend" / "Torso Twist"
			// Also inconsistent rotations
			// Incomplete. TODO.
			sortBy: "plane",
			0: [[[["37300","37302"]]]],
			1: [[[["37400","37402"]]]]
		},  
		"H6" : { // "Upper Body Tilts"
			//default: [ "3750R1", [[["3751R1", "3752R1"]]], [[["3753R1", "3754R1"]]] ],
			//"H6" : "alt2"
			"default": [[["3750R8", "3750R8"]]], // dup so that HO still works
			"H6" : {
				"default": [["3751R8", "3752R8"]],
				"H6" : {
					"default": ["3753R8", "3754R8"],
					"H6" : "alt1"
				}
			}
		},  
		"H7" : { // Much of the "Shoulder Hip Spine" section. I really don't
			     // understand these, though. :(
			sortBy: "rotation",
			"0": "36d1",
			"1": "36d2",
			"2": "36d11",
			"3": "36d2",
			"4": "36d12",
			"5": "36d22",
			"6": "36d13",
			"7": "36d22"
		},  
		"H8" : { // Shoulder stuff. Confusing.
			"default": [
				[ "36e0R5", "36e1R6", "36e3R6" ], // left shoulder up
				[ "36e1R5", "36d0R4", "36e2R5" ], // left shoulder neutral (n is broken)
				[ "36e3R5", "36e2R6", "36e0R6" ]  // left shoulder down
			],
			"H7" : "alt2", 
			"H8" : "alt3"
		},
		"HO" : "alt4", // alt5
		"HL" : generalActions.rotateLeft,
		"HR" : generalActions.rotateRight,
		"HP" : generalActions.togglePlane
	},
	
	punctuationSymbols = {
		"~." : "38830",
		"^." : "38820", 
		"L." : "38810",
		"(" : "38b00",
		")" : "38b04",
		"L," : "38710", 
		"^," : "38720",
		"~," : "38730",
		";" : "38900",
		":" : "38a00",
		"." : "38800",
		"," : "38700"
	};
	
	
	// ** KEYBOARD LAYOUTS **
	
	// TODO: Consider reworking these so that they're objects with 'init' 
	// functions, instead of just passing the functions straight.
	
	// Default keyboard layout.
	function BaseLayout() {
		// Lots of functions that are very particular to this layout are 
		// quite scattered across the script. TODO: Fix.
		// Specific variables and functions: actions, actionsMap, headMap, 
		// punctMap, finger, ...
		// Consider using the bottom-right key for spelling sequences.
		var actionsMap = 
			( "`  D  L  R  ↑  ? \\  /  ?  ↑  L  R  D     " +
			  "    P  ←  ↓  →  O  |  O  ←  ↓  →  P  F  ? " +
			  "     1  2  3  4  N  N  4  3  2  1  AC     " +
			  "      B  ☺  #  5  !  5  #  ☺  B   SS      " ).split(/\s+/g),
		// Consider: Moving all face contact to a separate section.
		// Consider: Requiring "back" buttons, or at least try to make them available more.
		// Not everyone knows about Ctrl-Z
		headMap = 
			( "`  D HL HR  ↑  ? \\  /  ?  ↑ HL HR  D     " +
			  "   HP  ←  ↓  → HO  | HO  ←  ↓  → HP  F  ? " +
			  "    H1 H2 H3 H4  N  N H4 H3 H2 H1  AC     " +
			  "     H5 H6 H7 H8  ! H8 H7 H6 H5   ?       " ).split(/\s+/g),
		punctMap = 
			( "`  D  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  D     " +
			  "    ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  F  ? " +
			  "     ? ~. ^. L.  (  ) L, ^, ~,  ;  ?      " +
			  "      ?  ?  ?  :  .  ,  ?  ?  ?  ?      " ).split(/\s+/g),
		blankMap = 
			( "`  D  ?  ?  ↑ \\  /  ?  ?  ↑  ?  ?  D     " +
			  "    ?  ←  ↓  →  |  ?  ?  ←  ↓  →  ?  F  ? " +
			  "     ?  ?  ?  ?  N  N ?  ?   ?  ?  ?      " +
			  "      ?  ?  ?  ?  ?  ?  ?  ?  ?  ?        " ).split(/\s+/g);
		
		var maps = {};
		maps[ symbolTypes.NONE ] = actionsMap;
		maps[ symbolTypes.HAND ] = actionsMap;
		maps[ symbolTypes.MOVEMENT ] = headMap;
		maps[ symbolTypes.DYNAMICS ] = headMap;
		maps[ symbolTypes.HEAD ] = headMap;
		maps[ symbolTypes.BODY ] = headMap;
		maps[ symbolTypes.PUNCTUATION ] = punctMap;
		maps[ symbolTypes.UNSPECIFIED ] = blankMap;
		
		function modeMap( symbol ) {
			return maps[ symbol.type ];
		}
		
		/**
		 * Returns the symbol list for the given type.
		 * @param {number} type Symbol type of the list to be returned.
		 */
		var modeSymbols = (function () {
			var modeSwitches = [];
			modeSwitches[ symbolTypes.BODY ] = bodySymbols;
			modeSwitches[ symbolTypes.MOVEMENT ] = movementSymbols;
			modeSwitches[ symbolTypes.HEAD ] = headSymbols;
			modeSwitches[ symbolTypes.DYNAMICS ] = dynamicsSymbols;
			
			return function modeSymbols( type ) {
				return modeSwitches[ type ] || {};
			};
		} )();
		
		// This seriously needs a better name.
		function symbolFSW() {
			var image = "";
			switch ( this.type ) {
				case symbolTypes.NONE:
					// Should this line be generic-ified?
					if ( !this.fake ) {
						return "";
					}
					// On the keyboard, act like uncreated symbols are hands.
				case symbolTypes.HAND:
					if ( this.fingers[ 0 ] === "0" ) {
						return "";
					} else {
						return handSymbolExtras( this.rightHand, this, this.fingers || "203" );
					}
				case symbolTypes.MOVEMENT :
				case symbolTypes.HEAD :
				case symbolTypes.BODY :
				case symbolTypes.DYNAMICS :
					var o = modeSymbols( this.type );
					for( var i = 0; i < 10; i++ ) {
						if( typeof o === "string" ) {
							image = o;
							break;
						} else {
							if ( o && o.sortBy ) {
								o = o[ this[ o.sortBy ] ] || o[ "default" ];
								i--;
								continue;
							}
							if ( o === undefined ) {
								//this.fake || console.log("?");
								o = "2ff";
								break;
							}
							o = o[ 
								this.alt[ i ] ] || 
								o[ o[ "default" ] ] || 
								o[ "default" ] || 
								o[ o.length - 1 ];
						}
					}
					
					image = symbolReplacements.call( this, image );
					
					while ( image && image.length < 5 ) {
						image += "0";
					}
					return image;
				case symbolTypes.PUNCTUATION:
					return punctuationSymbols[ this.alt[ 0 ] ];
			}
		}
		
		function symbolFromFSW( symbolCode, symbol ) {
			var targetSymbolText = "S" + symbolCode,
				symbolTopCode = symbolCode.substr( 0, 3 ),
				numericTopCode = parseInt( symbolTopCode, 16 );
			
			if ( symbolCode < "205" ) {
				// Search for handshape.
				// TODO: Work for "Between Palm Facings" (15b, handBetweenFacings)
				symbol.type = symbolTypes.HAND;
				symbol.bothHands = 0;
				
				if ( handShapes[ symbolTopCode ] ) {
					symbol.fingers = symbolTopCode;
				} else {
					// Note that this is still broken. 
					for ( var i in handBetweenFacings ) {
						if ( handBetweenFacings[ i ] === symbolTopCode ) {
							symbol.fingers = i;
						}
					}
					// return false;
				}
				// Assume we found the right symbol.
				// TODO: Rewrite this mess.
				outer:for ( var i = 0; i < 2; i++ ) {
					symbol.rightHand = !!i;
					for ( var ii = 0; ii < 8; ii++ ) {
						symbol.rotation = ii;
						for ( var iii = 0; iii < 2; iii++ ) {
							symbol.plane = iii;
							for ( var iiii = 0; iiii < 3; iiii++ ) {
								symbol.face = iiii;
								// TODO: Use handSymbolExtras here instead?
								symbol.updateImage();
								if ( symbol.symbolText === targetSymbolText ) {
									break outer;
								}
							}
						}
					}
				}
				if ( symbol.symbolText !== targetSymbolText ) {
					// console.log( "MISMATCH: ", symbol, symbol.symbolText, targetSymbolText );
					return false;
				}
				return symbol;
			} else {
				// Run through pretty much everything.
				// I'll add some stuff to speed this up later.
				// Consider splitting this off into a "reverse searchTree" 
				// function, which could be exposed.
				//
				// Double and triple floor-plane arrows are broken. TODO: Fix.
				// Also triple alternating wall-plane arrows.
				
				switch ( true ) {
					case symbolCode < "2f7":
						symbol.type = symbolTypes.MOVEMENT;
						break;
					case symbolCode < "2ff":
						symbol.type = symbolTypes.DYNAMICS;
						break;
					case symbolCode < "36d":
						symbol.type = symbolTypes.HEAD;
						break;
					case symbolCode < "37f":
						symbol.type = symbolTypes.BODY;
						break;
					case symbolCode < "387":
						// "Detailed location"... Use UNSPECIFIED for now.
						return false;
					case symbolCode < "38c":
						symbol.type = symbolTypes.PUNCTUATION;
						for ( var i in punctuationSymbols ) {
							if ( punctuationSymbols[ i ] === symbolCode ) {
								symbol.alt[ 0 ] = i;
								symbol.updateImage();
								return symbol;
							}
						}
						// ummm
					default:
						// ??
						return false;
					// TODO: Punctuation, others
				}
				
				// TODO: Real variable names.
				function findMatchingSymbol( i, level, symbol ) {
					var x = alts.split( "/" ), z,
						presetProps = {};
					for ( var ii = 1, skips = 1, y; ii < x.length; ii++ ) {
						y = x[ ii ];
						if ( y.indexOf( "=" ) === -1 ) {
							symbol.alt[ ii - skips ] = y === "default" ? 0 : y;
							level = level[ y ];
						} else {
							z = y.split( "=" );
							// Assuming the only options are boolean or number.
							symbol[ z[ 0 ] ] = isNaN( +z[ 1 ] ) ? z[ 1 ] === "true" : +z[ 1 ];
							presetProps[ z[ 0 ] ] = true;
							skips++;
							level = level[ y.split( "=" )[ 1 ] ];
						}
					}
					return testThing( symbol, level, presetProps );
				}
				
				function testThing( symbol, x, presetProps ) {
					var reg = new RegExp( x.replace( /R[1-8HBF][HFE]?/g, "." ) );
					if ( reg.test( targetSymbolText ) ) {
						// TODO: Predict the necessary property values from
						// the RX.
						
						var preRotate = "rotation" in presetProps,
							preFace = "face" in presetProps,
							preRightHand = "rightHand" in presetProps || "bothHands" in presetProps;
						outer:for ( var i = 0; i < ( preRightHand || 3 ); i++ ) {
							if ( !preRightHand ) {
								symbol.rightHand = !!i;
							}
							
							if ( !( "bothHands" in presetProps ) ) {
								// It might be better to start off with bothHands.
								symbol.bothHands = +( i === 2 );
							}
							
							for ( var ii = 0; ii < ( preRotate || 8 ); ii++ ) {
								if ( !preRotate ) {
									symbol.rotation = ii;
								}
								for ( var iiii = 0; iiii < ( preFace || 4 ); iiii++ ) {
									if ( !preFace ) {
										symbol.face = iiii;
									}
									
									// if ( symbolReplacements.call( symbol, x ) === symbolCode ) {
									// If this turns out to be substantially slower,
									// I'll just have something run through the
									// pile of symbols and add 0s where necessary.
									// TODO: Check performance.
									if ( symbolCode.lastIndexOf( symbolReplacements.call( symbol, x ), 0 ) === 0 ) {
										
										//symbol.updateImage();
										return symbol.symbolText === targetSymbolText;
										
										// Does this work?
										// return symbolCode === symbolFSW.call( symbol );
									}
								}
							}
						}
						
						
						return false;
					} else {
						return false;
					}
				}
				
				var level = modeSymbols( symbol.type ),
					allAlts = [].concat( generateReverseDB()[ symbolTopCode ] ),
					// allAlts = cloneArray( generateReverseDB()[ symbolTopCode ] ),
					alts,
					success;
				
				for( var i = 0; i < allAlts.length; i++ ) {
					alts = allAlts[ i ];
					// console.log( "alts=", alts );
					// v This is the important line here.
					success = alts && findMatchingSymbol( 0, level, symbol );
					if ( success ) {
						break;
					}
				}
				
				if ( !success ) {
					// Could not find the symbol anywhere.
					// notFound();
					return false;
				} else {
					return symbol;
				}
			}
		}
		
		// Toggle one finger of a hand symbol up or down.
		// Used by the keys "1", "2", "3", "4", and "5", in the actions object
		// below. (I should change those IDs.)
		var preCombos = { "false": { symb: false, fingers: 0 }, "true": { symb: false, fingers: 0 } };
		function finger( which ) {
			return function ( rightHand, position, alt, shiftKey ) {
				/*
				For test:
				
var a = {}, qjk = handShapes;

function q(j, k, x) {
	for (var i = 0, e; i < j.length; i++) {
		e = j[ i ]; // handshape code
		if ( e ) {
			a[ e ] = a[ e ] || [ 50, "..." ];
			if ( k < a[ e ][ 0 ] ) {
				a[ e ] = [ k, x + ">" + i ];
				if ( qjk[ e ] ) {
					q( qjk[ e ], k + 1, x + ">" + i );
				}
			}
		}
	}
}
q( qjk["203"], 1, "");
p=0;y=[];
for ( var i in qjk ) {
	p++;
}
for ( var f = 256; f < 516; f++ ) {
	if ( !qjk[ f.toString( 16 ) ] ) {
		y.push( f.toString( 16 ) );
	}
}
a;y;

				*/
				
/*

a = {}; qjk = handShapes;

function add( a, o ) {
	if ( a.indexOf( o ) === -1 ) {
		a.push( o );
	}
}

function findCombos( h ) {
	
}

function q( h, steps, stepsList ) {
	var o = a[ h ] = a[ h ] || {};
	o.j = steps;
	o.r = stepsList;
	if ( !o.l ) {
		o.l = [];
		for ( var i = 16; i--; ) {
			var b = [];
			for ( var ii = 8; ii > 0; ii >>= 1 ) {
				if ( i & ii ) {
					b.push( Math.log2( ii ) );
				}
			}
			//console.log( 999, b );
			// um, stuff
			if ( handShapes[ h + "-" ] && handShapes[ h + "-" ][ i ] ) {
				add( o.l, handShapes[ h + "-" ] && handShapes[ h + "-" ][ i ] );
			} else {
				var val = false;
				function combos( ar, map ) {
					ar.forEach( function ( a, ind, bdup ) {
						bdup.splice( ind, 1 );
						if ( ar.length ) {
							combos( ar, handShapes[ map[ a ] ] );
						} else {
							if ( val === false ) {
								val = map[ a ];
							} else if ( val !== map[ a ]  ) {
								val = "_";
							}
						}
					} );
				}
				combos( b, handShapes[ h ] );
				if ( val && val !== "_" ) {
					add( o.l, val );
				}
			}
		}
		for ( var i = 4; i < ( steps === 0 ? 5 : 8 ); i++ ) {
			if ( handShapes[ h ][ i ] ) {
				add( o.l, handShapes[ h ][ i ] );
			}
		}
	}
	for ( var i = 0; i < o.l.length; i++ ) {
		if ( !a[ o.l[ i ] ] || a[ o.l[ i ] ].j > steps + 1  ) {
			q( o.l[ i ], steps + 1, stepsList + "/" + "" )
		}
	}
}

q( "203", 0, "start" );

a;

*/
				
				var result;
				if ( which < 6 ) {
					// Finger combos.
					// TODO: Make cleaner.
					var preCombo = preCombos[ rightHand ];
					if ( preCombo.symb !== false ) {
						var possCombo = handShapes[ preCombo.symb + "-" ];
						if ( possCombo ) {
							var changedFingers = preCombo.fingers | ( 1 << which - 1 );
							if ( !this.fake ) {
								preCombo.fingers = changedFingers;
							}
							if ( possCombo[ changedFingers ] ) {
								result = possCombo[ changedFingers ];
							}
						}
					} else {
						if ( !this.fake ) {
							preCombo.symb = this.fingers;
							keyboard.keyupActions[ position ] = function () {
								preCombo.fingers = 0;
								preCombo.symb = ( ( handShapes[ preCombo.symb + "-" ] && keyboard.update( rightHand ) ), false );
							};
							preCombo.fingers |= 1 << which - 1;
						}
					}
				}
				result = result || ( handShapes[ this.fingers ] || [] )[ which - 1 ] || this.fingers;
				
				if ( result === this.fingers || !result ) {
					return "";
				}
				
				this.fingers = result;
				
				// TODO: If hold button for more than two seconds, create box
				// next to key for "basic" selection of alt handshape. Grayish
				// box next to key, blue selection, arrow keys for navigation.
				
				if ( this.type === symbolTypes.NONE ) {
					this.type = symbolTypes.HAND;
					this.X += rightHand ? 50 : -50;
					this.buildElement();
				}
			};
		}
		
		// Used by the keys "N" and "D". ("Next symbol" and "Delete symbol".)
		function addSwitchOrDeleteSymbol ( rightHand, position, alt, shiftKey ) { 
			// This handles both the "Delete/Dynamics" and the "Next symbol" keys.
			// The "New symbol" button is also used for cycling through
			// existing symbols in the list.
			// If we're last in the line of actual symbols, create a new one.
			// If we're on a not-yet-created symbol, go to the first symbol.
			// Otherwise, make the next symbol in line the active one.
			// Note: Remember to put this in the visible docs. It's probably
			// not so intuitive.
			// Should this trigger updatePosition()? I'm thinking yes.
			// Consider positioning symbols next to recent symbol.
			var isDelete = alt === "D",
				symbols = activeSign.symbols, 
				i = symbols.indexOf( this );
			
			if ( shiftKey ) {
				if ( this.type !== symbolTypes.NONE ) {
					
				}
				// return;
			}
			
			if ( isDelete ) {
				if ( this.type === symbolTypes.NONE ) {
					// Go to Dynamics menu
					this.type = symbolTypes.DYNAMICS;
					this.alt[ 0 ] = "H4";
					if ( !this.fake ) {
						this.buildElement();
					}
					return;
				} else {
					if ( this.fake ) {
						return rightHand ?
							i18n.ase.deleterightsymbol :
							i18n.ase.deleteleftsymbol; // Override default image, blank the key.
					} else {
						this.remove();
						activeSign.updatePosition();
					}
				}
			} else {
				if ( this.fake ) {
					return shiftKey ? i18n.ase.duplicate : ( this.rightHand ? i18n.ase.nextsymbolright : i18n.ase.nextsymbolleft );
				}
				if ( this.type === symbolTypes.NONE ) {
					// Remove the "empty" symbol spot.
					// Duplication of above. TODO: Fix.
					symbols.splice( i, 1 );
				} else {
					// Switching from an actual symbol, so make it inactive.
					this.blur();
					// If shift key is pressed, clone symbol.
					if ( shiftKey ) {
						symbols.splice( i + 1, 0, this.clone() );
						var newSymbol = symbols[ i + 1 ];
						newSymbol.X += rightHand ? -5 : 5;
						newSymbol.Y += 5;
						newSymbol.buildElement( true );
					}
				}
				// Is this line necessary?
				i = symbols.indexOf( this ) + 1;
			}
			
			// Select the next rightHand/bothHands-matching symbol in symbols.
			for ( ; i < symbols.length; i++ ) {
				if ( activeSymbols[ !rightHand ] !== symbols[ i ] ) {
					if ( symbols[ i ].bothHands ) {
						symbols[ i ].rightHand = rightHand;
					}
					if ( symbols[ i ].rightHand === rightHand ) {
						symbols[ i ].focus( rightHand );
						break;
					}
				}
			}
			
			// Couldn't find one later in line. Start a new symbol.
			if ( i === symbols.length ) {
				symbols.push( 
					activeSymbols[ rightHand ] = new SignSymbol( null, activeSign )
				);
				symbols[ i ].rightHand = rightHand;
			}
			
			if ( isDelete && this.type === symbolTypes.PUNCTUATION ) {
				// TODO
				activeSign.lane = "M";
				keyboard.blank();
				keyboard.update( true );
			}
		}
		
		function symbolReplacements( image ) {
			var replacements = {
				"RH"  : 1 - this.rightHand,
				"RBH" : this.bothHands ? 2 : 1 - this.rightHand,
				"RBF" : ( this.face & 2 ? 0 : 3 ) + ( this.bothHands ? 2 : 1 - this.rightHand ),
				"RBE" : ( this.bothHands ? 0 : 2 - this.rightHand ),
				"R2F" : ( this.face & 1 ? 2 + ( this.rotation & 1 ) : this.rotation & 1 ),
				"R2"  : this.rotation % 2,
				"R4F" : ( this.face & 1 ? 4 + ( ( 8 - this.rotation ) % 4 ) : this.rotation % 4 ),
				"R4"  : this.rotation % 4,
				"R8H" : ( this.rotation + this.rightHand * 8 ),
				"R8F" : ( this.face & 1 ? 8 + ( ( 8 - this.rotation ) % 8 ) : this.rotation ),
				// These two are the bizarre ones for the body section.
				"R5"  : ( ( this.rotation % 2 ) && ( this.rotation % 4 === 1 ? 1 : 2 ) ),
				"R6"  : ( ( ( this.rotation % 2 ) && ( this.rotation % 4 === 1 ? 1 : 2 ) ) + 3 ),
				"R8"  : this.rotation
			};
			
			image = image.replace( /R[1-8HBF][HFE]?/g, function ( a ){
				return replacements[ a ].toString( 16 );
			});
			
			return image;
		}
		
		// Used by symbolFromFSW.
		// Maybe needs a bit of cleanup.
		var reverseDB = {};
		function generateReverseDB() {
			if ( reverseDB.done ) {
				return reverseDB;
			}
			// TODO: Real variable names.
			function x( y, z ) {
				for ( var i in y ) {
					switch( typeof y[ i ] ) {
						case "object":
							if ( i !== "sortBy" && !y[ y[ i ] ] ) {
								x( y[ i ], 
									y.sortBy ? 
										z + "/" + y.sortBy + "=" + i :
										z + "/" + i
								);
							}
							break;
						case "string":
							//reverseDB[ y[ i ].substr( 0, 3 ) ] = z;
							// I am overusing switch. This is strange. 
							var w = y[ i ].substr( 0, 3 );
							var zz = 
									y.sortBy ? 
										z + "/" + y.sortBy + "=" + i :
										z + "/" + i;
							switch ( typeof reverseDB[ w ] ) {
								case "string":
									reverseDB[ w ] = [ reverseDB[ w ] ];
								case "object":
									reverseDB[ w ].push( zz );
									break;
								case "undefined":
									reverseDB[ w ] = zz;
							}
							break;
					}
				}
			}
			x( movementSymbols, "" );
			x( headSymbols, "" );
			x( bodySymbols, "" );
			x( dynamicsSymbols, "" );
			reverseDB.done = true;
			return reverseDB;
		}
	
		function searchTree( rightHand, position, alt ) {
			// Needs real variable names.
			
			// In certain conditions, the alt cycles 
			// (ie, alt[x] = 0 > alt[x] = 1 > ... > alt[x] = length - 1 > alt[x] = 0 )
			// In others, it becomes the key, that is, "alt" the argument.
			// How to tell the difference? If you hit a "alt#" value, you
			// cycle. Otherwise, switch. Simple as that. 
			// (Sorry, documenting this is difficult.)
			
			// Do we mine for the current position or what?
			
			// 
			var baseLevel = modeSymbols( this.type ),
				currentLevel = baseLevel, // either an object, string, or undefined
				prevLevel, // This attribute is not actually used atm.
				i;
			
			var log = this.fake ?
				function(){} :
				function() {
					//console.log.apply( console, arguments );
				};
			// log( currentLevel );
			
			// TODO:
			// If we can't get to a branch, set the other stuff to zero. Sometimes.
			// BUG: HL and HR reverse symbols when righthand.
			// Hopefully I won't have to use sortBy in each of the 3 cases.
			
			// This needs a bit of cleanup, I think.
			// ... I no longer remember how this section works, and I didn't 
			// comment this sufficiently. Hmmm...
			
			var changeLevel, // unused. probably will be removed.
				altToToggle, // number or function
				altToLoop, // number
				altValue,
				depth;
			loop:for ( i = 0; i < 10; i++ ) {
				switch ( typeof currentLevel ) {
					case "object" :
						if ( currentLevel.sortBy ) {
							currentLevel = 
								currentLevel[ this[ currentLevel.sortBy ] ] || 
								currentLevel[ "default" ];
							i--;
							continue loop;
						}
						
						if ( altToLoop === i && !altToToggle ) {
							// This is our second time around, and we hit the
							// "loop this" target.
							this.alt[ altToLoop ]++;
							if ( !currentLevel[ this.alt[ altToLoop ] ] ) {
								this.alt[ altToLoop ] = 0; //currentLevel.length - 1;
							}
						}
						
						if ( alt in currentLevel && ( !depth || depth < i ) ) {
							if ( /^alt\d+/.test( currentLevel[ alt ] ) ) {
								if ( !altToLoop || depth < i ) {
									altToToggle = undefined;
									depth = i;
									altToLoop = currentLevel[ alt ].substr( 3 );
									altValue = altToLoop.split("=");
									if ( altValue.length === 2 ) {
										this.alt[ altValue[ 0 ] ] = 
											this.alt[ altValue[ 0 ] ] === altValue[ 1 ] ?
												0 : altValue[ 1 ];
										break loop;
									}
									altToLoop = + altToLoop;
									// log( "set altToLoop = " + altToLoop );
									
									// Start back from the beginning, so that we 
									// can find the alt to loop.
									i = -1; currentLevel = baseLevel;
									continue loop;
								}
							} else {
								if ( typeof currentLevel[ alt ] === "function" ) {
									altToToggle = currentLevel[ alt ];
									// break loop;
								} else {
									altToToggle = i;
								}
								depth = i;
							}
						}
						prevLevel = currentLevel;
						currentLevel = currentLevel[ this.alt[ i ] ] ||
							currentLevel[ currentLevel[ "default" ] ] || 
							currentLevel[ "default" ] ||
							currentLevel[ i > altToLoop ?
								( this.alt[ i ] = currentLevel.length - 1 ) :
								currentLevel.length - 1 
							];
						break;
					case "string" :
						if ( !altToLoop || i > depth ) {
							if ( typeof altToToggle === "function" ) {
								altToToggle.apply( this, arguments );
							} else {
								this.alt[ altToToggle ] = 
										this.alt[ altToToggle ] === alt ? 0 : alt;
							}
							currentLevel = undefined;
							//break loop;
						} else {
							currentLevel = undefined;
						}
						break;
					case "undefined" :
						this.alt[ i ] = 0;
						break;
				}
			}
			
			// log( this );
				
		}
		
		// Actions list for the default layout. This maps keys to functions.
		// Several of these call functions which return other sub-functions.
		// Each of these functions are called with the following four arguments:
		// rightHand: Boolean, is true if the key is not the first of its icon in the layout.
		// position: Number, position of the key pressed on the keyboard.
		// key icon: String
		// shiftKey
		// "this" maps to the SignSymbol.
		// If a return value is given, it overrides the image for the key.
		var actions = {
			"1" : finger( 1 ), // Little finger
			"2" : finger( 2 ), // Ring
			"3" : finger( 3 ), // Middle
			"4" : finger( 4 ), // Index
			"5" : finger( 5 ), // Thumb
			"←" : generalActions.moveLeft, 
			"↓" : generalActions.moveDown, 
			"→" : generalActions.moveRight, 
			"↑" : generalActions.moveUp, 
			"O" : function ( rightHand, position, alt, shiftKey ) {
				this.face = ( 
					this.face + 
					4 + 
					// When "Between facings" symbols are available, use them.
					//( handBetweenFacings[ this.fingers ] ? 3.5 : 3 ) * 
					// No longer using handBetweenFacings
					3 * 
					// Other direction when the shift key is held.
					( shiftKey ? -1 : 1 )
				) % 4;
			},
			"L" : generalActions.rotateLeft,  // Rotate counter-clockwise
			"R" : generalActions.rotateRight, // Rotate clockwise
			"P" : generalActions.togglePlane,
			"\\": generalActions.leftLane, // Left lane
			// "|" : generalActions.middleLane,  // Middle lane (default)
			"|" : function ( rightHand, position, alt ) {
				if ( keyboard.shiftKey ) {
					// Switch hands.
					var other = activeSymbols[ true ];
					other.rightHand = false;
					this.rightHand = true;
					other.focus();
					this.focus();
					//( activeSymbols[ true ] = this ).rightHand = true;
					//( activeSymbols[ false ] = other ).rightHand = false;
					this.updateImage();
					keyboard.update( true );
				} else {
					generalActions.middleLane.call( this );  // Middle lane (default)
				}
			},
			"/" : generalActions.rightLane,  // Right lane
			"F" : toggleFingerSpelling,
			"N" : addSwitchOrDeleteSymbol, // Add new symbol, or select symbol
			"D" : addSwitchOrDeleteSymbol, // Delete symbol, or go to Dynamics
			"!" : function ( rightHand ) { // Punctuation
				// BROKEN, in numerous ways.
				//var symbols = activeSign.symbols;
				if ( activeSign.isEmpty() ) {
					this.type = symbolTypes.PUNCTUATION;
					this.alt[ 0 ] = ".";
					if ( !this.fake ) {
						// Move the punctuation symbol to the middle (roughly).
						this.X = 464;
						this.Y = 496;
						activeSign.lane = "";
						this.buildElement();
						
						// TODO: Deal with keyboard.
						// Not a good long-term solution.
						keyboard.blank();
					}
				} else {
					if ( this.fake ) {
						// Blank the key.
						return "";
					}
				}
			},
			// Would it be possible to merge parts of these three methods?
			"☺" : function ( rightHand, position ) { // Head, or hand alternates
				if ( this.type === symbolTypes.NONE ) {
					// Default to center position.
					this.X = this.Y = Math.round( 500 - ( 36 / 2 ) );
					
					// Look for earlier head symbols.
					for ( var symb, symbs = activeSign.symbols, i = symbs.length; i--; ) {
						symb = symbs[ i ];
						// Find the latest symbol on the same hand.
						if ( symb.type === symbolTypes.HEAD ) {
							// Place movement symbol above old symbol if 
							// pointing upward.
							this.Y = symb.Y;
							this.X = symb.X;
							break;
						}
					}
					this.type = symbolTypes.HEAD;
					this.face = 0;
					this.bothHands = 1;
					this.buildElement();
				} else {
					return finger( 7 ).call( this, rightHand );
				}
			},
			"#" : function ( rightHand, position ) { // Movement, or hand alternates
				if ( this.type === symbolTypes.NONE ) {
					// IDEA: Position movement symbols above the previous 
					// symbol if pointing up, next to it if pointing sideways, 
					// etc. This will require some complicated stuff, I think,
					// some of which will also be necessary for SWriter mode.
					// Actually, dimensions for this are always more-or-less the
					// same, so maybe it's simple.
					
					// Attempt 1:
					if ( !this.fake ) {
						for ( var symb, symbs = activeSign.symbols, i = symbs.length; i--; ) {
							symb = symbs[ i ];
							// Find the latest symbol on the same hand.
							if ( symb.type !== symbolTypes.NONE && symb.rightHand === rightHand /* && this.rotation === 0 */ ) {
								// Place movement symbol above old symbol if 
								// pointing upward, to the right if pointing to
								// the right, etc. The new movement symbol is
								// approximated to be about 15px long, and we 
								// add 5px of padding.
								
								var q = { 0 : 0, 1 : 0, 2 : 0.5, 3 : 1, 4 : 1, 5 : 1, 6 : 0.5, 7 : 0 };
								
								this.Y = symb.Y + Math.floor(
									( symb.height + 5 ) * q[ this.rotation ] - 
									20 * q[ ( this.rotation + 4 ) % 8 ]
								);
								this.X = symb.X + Math.floor(
									( symb.width + 5 ) * q[ ( this.rotation + 6 ) % 8 ] - 
									20 * q[ ( this.rotation + 2 ) % 8 ]
								);
								/*
								this.Y = symb.Y - 15 - 3;
								this.X = Math.floor( symb.X + symb.width / 2 - 13 / 2 );
								*/
								break;
							}
						}
					}
					
					this.type = symbolTypes.MOVEMENT;
					this.buildElement();
				} else {
					return finger( 8 ).call( this, rightHand );
				}
			},
			"B" : function ( rightHand ) { // Body, or hand bent finger(s) toggle
				if ( this.type === symbolTypes.NONE ) {
					this.type = symbolTypes.BODY;
					this.X = 500 - ( 42 / 2 );
					this.buildElement();
				} else {
					return finger( 6 ).call( this, rightHand );
				}
			},
			"AC" : function ( rightHand, position ) { // Autocomplete
				// The display of this key is run partly by hooks.keyAction.
				
				var suggestion = autocomplete.suggestLoop( !this.fake );
				
				// Issues: This needs to update even just on movement of a 
				// symbol. Needs to work when just one suggestion. Don't do
				// fast repeated requests.
				
				if ( this.fake ) {
					// Don't use normal return, so we can set sizing.
					if ( suggestion ) {
						keyboard.setKeyImageToFSW( position, null, suggestion, 0.5 );
						return false;
					} else {
						// No suggestions. Blank the key.
						return '';
					}
				} else {
					if ( suggestion ) {
						autocomplete.useSuggestion( suggestion );
					}
				}
			},
			// TODO: Remove this. It was for testing purposes.
			"FSW" : function () {
				if ( !this.fake ) {
					var s = this;
					console.log( "STATUS:" +
						"\nY = " + s.Y +
						"\ntop = " + activeSign.top +
						"\nSign height = " + activeSign.height +
						"\nsymb height = " + s.height
					);
				}
			},
			"SS" : function () { // SignSpelling Sequence
				if ( this.fake ) {
					// TODO: Get ASL sign for this, add to i18n.
					return '';
				} else {
					var sign = this.sign;
					if ( !sign.isEmpty() ) {
						if ( sign.mode ) {
							sign.mode = false;
						} else {
							var ss = sign.spellingSequence,
								symbols = sign.symbols,
								symbolIndexes = [];
							
							sign.mode = { 'type': 'ss', symbolIndexes: symbolIndexes };
							
							if ( ss ) {
								fswParser.getSymbolsFromSequence( 
									ss,
									function ( symbolText ) {
										for ( var i = 0; i < symbols.length; i++ ) {
											if ( symbols[ i ].symbolText === symbolText && symbolIndexes.indexOf( i ) === -1 ) {
												symbolIndexes.push( i );
												return;
											}
										}
										// The sequence includes a symbol not in
										// the sign. Add raw symbolText.
										symbolIndexes.push( symbolText );
									}
								);
							}
						}
						keyboard.blank();
						keyboard.toggleAllKeyTexts( !sign.mode );
						keyboard.update();
					}
					
					/*
					var ss = "A", 
						symbols = this.sign.symbols;
					for ( var i = 0; i < symbols.length; i++ ) {
						ss += symbols[ i ].symbolText;
					}
					this.sign.spellingSequence = ss;
					*/
				}
			},
			"PT" : function () { // ` key
				caret.togglePlaintext();
			}
		};
		
		// Punctuation, movement, body, head, and dynamics actions. Needs replacing.
		( function () {
			function punctuationAction( rightHand, position, alt ) {
				this.alt[ 0 ] = alt;
				// TODO: Consider having one of these symbols call keyboard.blank.
				// Need to do something about lingering symbols and such.
			}
			for ( var i in punctuationSymbols ) {
				actions[ i ] = punctuationAction;
			}
			
			for ( i in headSymbols ) {
				actions[ i ] = searchTree;
			}
		})();
		
		var layout = {
			label: "Default", // TODO: i18n
			baseKeyboardLayout : actionsMap,
			baseKeyboardActions: actions,
			modeMap: modeMap,
			// Keys that don't have images.
			imagelessKeys: 
				[ "↑", "←", "↓", "→", /*"F",*/ /*"AC",*/ "?", "\\", "|", "/", "`" ],
			// Keys that don't trigger a keyboard update when pressed.
			suppressUpdateKeys: [ "↑", "←", "↓", "→" ],
			// Those containing words will probably be replaced by SW symbols.
			keyTexts: {
				"\\": "Left lane", 
				"|" : "Middle lane",
				"/" : "Right lane",
				/*
				"F" : "Fingerspelling mode",
				"N" : "Next symbol",
				"D" : function ( rightHand ) {
					return "Delete " + ( rightHand ? "righthand" : "lefthand" ) + " symbol";
				},
				*/
				"←" : "←",
				"↓" : "↓",
				"→" : "→",
				"↑" : "↑",
				//"AC": "Autocomplete",
				//"R" : "↻",
				//"L" : "↺"
				"`" : config.ce ? "Toggle plaintext" : "",
				"SS": "Sign-Spelling sequence"
			},
			symbolFSW: symbolFSW,
			symbolFromFSW: symbolFromFSW,
			keyboardOverrideDisplay: function () {
				var sign = activeSign,
					mode = sign.mode;
				if ( mode ) {
					if ( mode.type === 'fs' ) {
						var fingerspellTogglePosition = actionsMap.indexOf( "F");
						return function ( position ) {
							return position === fingerspellTogglePosition ?
								i18n.ase.fingerspell :
								( keyboard.shiftKey &&
									keyboard.currentLanguageData.shiftLetters && 
									keyboard.currentLanguageData.shiftLetters[
										keyboard.getLetter( position )
									] || 
									keyboard.currentLanguageData.letters[ 
										keyboard.getLetter( position )
									]
								) || "";
						};
					} else if ( mode.type === 'ss' ) {
						// Keyboard display when editing signspelling sequences.
						var symbolIndexes = mode.symbolIndexes,
							symbols = sign.symbols;
						
						return function ( position ) {
							if ( position > 36 || position < 13 ) {
								// Top or bottom row. Blank keys.
								return '';
							} else if ( position < 26 ) {
								// Upper row. Display symbols that have not yet
								// been added to the SignSpelling sequence.
								var symbol = sign.getVisibleSymbol( position - 13 );
								// TODO: Simplify this line.
								if ( symbol && symbolIndexes.indexOf( symbols.indexOf( symbol ) ) === -1 ) {
									return symbol.symbolText;
								} else {
									return '';
								}
							} else {
								// Lower row. Display symbols from the current 
								// sign's signspelling sequence.
								var symbolIndex = symbolIndexes[ position - 26 ],
									symbol = symbols[ symbolIndex ];
								return symbol ? symbol.symbolText : symbolIndex || '';
							}
						};
					}
				}
			},
			keyboardOverrideActivate: function ( symb, position ) {
				var sign = activeSign,
					mode = sign.mode;
				if ( mode ) {
					if ( mode.type === 'fs' && symb !== "F" ) {
						fingerspell( position );
						return true;
					} else if ( mode.type === 'ss' && symb !== "SS" ) {
						if ( !sign.isEmpty() ) {
							var symbolIndexes = mode.symbolIndexes,
								symbols = sign.symbols;
							if ( position > 12 && position < 26 ) {
								// Selected key on the upper row. Add symbol to
								// the sequence.
								var symbol = sign.getVisibleSymbol( position - 13 );
								if ( symbol ) {
									var index = symbols.indexOf( symbol ),
										notYetSelected = symbolIndexes.indexOf( index ) === -1;
									
									if ( notYetSelected ) {
										symbolIndexes.push( index );
										sign.spellingSequence = sign.spellingSequence || 'A';
										sign.spellingSequence += symbol.symbolText;
										keyboard.update();
									}
								}
							} else if ( position > 25 && position < 37 ) {
								// Selected key on the lower row. Remove selected
								// symbol from the sequence.
								
								if ( symbolIndexes.length > position - 26 ) {
									var sequence = '',
										i,
										symbolIndex;
									symbolIndexes.splice( position - 26, 1 );
									// Update sign.spellingSequence.
									for ( i = 0; i < symbolIndexes.length; i++ ) {
										symbolIndex = symbolIndexes[ i ];
										if ( symbolIndex.length === 6 ) {
											// We don't actually have this
											// symbol in the sign. Use raw
											// content.
											sequence += symbolIndex;
										} else if ( symbolIndex in symbols ) {
											// Normal index.
											sequence += symbols[ symbolIndex ].symbolText;
										} else {
											// I have no idea. Something broke.
											console.error( "SignSequence error" );
										}
									}
									sign.spellingSequence = sequence ?
										'A' + sequence : 
										'';
									keyboard.update();
								}
							}
						}
						// Suppress "regular" key action.
						return true;
					}
				}
			},
			hooks: {
				space: function () {
					if ( 
						signArea.signs[ activeSign.index() - 1 ].mode &&
						signArea.signs[ activeSign.index() - 1 ].mode.type === 'fs'
					) {
						toggleFingerSpelling( null, null, "F" );
					}
				},
				changeSign: function ( previousSign, targetSign ) {
					// TODO: Work for signspelling sequences.
					// TODO: Check if this runs before or after .space, and 
					// whether extra unnecessary work is done.
					var currentlyFS = !!targetSign.mode;
					if ( currentlyFS === !previousSign.mode ) {
						// TODO: Fix keyboard mess.
						// TODO: Make sure this works for NSTs.
						keyboard.blank();
						keyboard.toggleAllKeyTexts( !currentlyFS );
						keyboard.update();
					}
				},
				keyAction: ( function () {
					var lastCheckedTimestamp = 0,
						pending = false,
						position = actionsMap.indexOf( 'AC' ),
						lastCheckedSignText = '';
					
					return function () {
						// Autocomplete. Show suggestions on autocomplete key.
						
						var now = Date.now();
						
						function check() {
							// Find suggestions to show on the key.
							
							lastCheckedTimestamp = now;
							
							// TODO: Use symbols.join() instead, so that we
							// don't get duplicate searches after images load
							// and the Sign expands.
							var currentSignText = activeSign.toString();
							//var currentSignText = activeSign.symbols.join();
							
							if ( lastCheckedSignText === currentSignText || activeSign.mode ) {
								// Don't run this again unless the text has 
								// changed and we're not fingerspelling.
								return;
							}
							
							lastCheckedSignText = currentSignText;
							
							// TODO: Maybe resize suggestion to fit key?
							
							if ( !autocomplete.hasSuggestions( currentSignText ) ) {
								// Not currently running through a loop.
								autocomplete.findSuggestions( function ( s ) {
									if ( s ) {
										keyboard.setKeyImageToFSW( position, null, autocomplete.suggestLoop(), 0.5 );
									} else {
										// None found. Blank the key.
										keyboard.setKeyImageToFSW( position, null, '', 0.5 );
									}
								} );
							}
						}
						
						if ( now - lastCheckedTimestamp > 500 ) {
							check();
							// Don't smash the server with a thousand requests,
							// but don't ignore changes indefinitely either.
							setTimeout( function () {
								if ( pending ) {
									// Another key press happened within the 500ms. Check for new
									// suggestions again.
									check();
									pending = false;
								}
							}, 500 );
						} else {
							// Changed the sign, and not querying immediately, so we're behind
							// on suggestions. Will check when the last setTimeout timer runs
							// out.
							pending = true;
						}
						return false;
					};
				} )()
			}
		};
		
		return layout;
	}
	
	// Currently incomplete layout, to be based off SignWriter DOS.
	function DOSLayout() {
		// Layout to be based off of SignWriter DOS.
		// TODO: Need to ask about the license of the keyboard layout.
		// This layout can be activated either by ?use-dos in the URL or by
		// clicking "DOS" in the layouts section of the settings menu.
		
		var symbMaps = {
			K1: [ , "1000", "1061", "10a1", "1010", , , "1051" ],
			K2: [ , "10e0", "1101", "1150", "1181", , , "1191", "11a0", "11c0", "11d0" ],
			K3: [ , "11e0", "1221", "1281", "1291", "1381", "13c0", "12d0", "13d1", "13f1", "1401" ],
			K4: [ , "1440", "1450", "1470", "14a0" ],
			K5: [ , "14c0", "1500", "14e0", "1520", , , "15a0", "15d0", "15f0", "1600" ],
			K6: [ , "1851", "17d1", "1801", "1821", "1571", , "1661", "1691", "1671", "1681" ],
			K7: [ , "16d1", "1531", "16f1", "1711", , , "1760", "1741", "1771" ],
			K8: [ , "1870", "1860", "18b0", "18c0", , ,"18e0", "18d1" ],
			K9: [ , "1920", "1980", "19a0", "19b1", , , "19c0", "1a00" ],
			KA: [ , "1a50", "1ab0", "1ae0", "1b00" ],
			KB: [ , "1bb0", "1c40", "1c11", "1c51", , , "1c21", "1c60", "1c90" ],
			KC: [ , "1ce0", "1d50", "1cf0", "1d11", , , "1d41", "1d31", "1d21" ],
			KD: [ , "1da1", "1d91", "1d81", "1d71", "1d61", , "1dc0", "1de0", "1e00", "1e10" ],
			KE: [ , "1ea1", "1eb1", "1ec1", "1ed1", , , "1f11", "1ef1", "1f41", "1f21" ],
			KF: [ , "1f50", "1f90", "1f70", "2030", , , "1fb0", "1ca0", "2010", "1fa0" ],
			M1: [ , "25d0", "26104", "2a601", "2a603", "2b001", "2b002", "2ad00", "2ad02", "2a900", "2a902"],
			M2: [ , "22f04", "23108", "2330a", "23404", "23604", "27100", "26a0", "26c08", "26e06", "26f0"],
			M3: [ , "22a04", "23804", "23b04", "23f03", "24c04", "24d0c", "24202", "2450a", "2480a", "24b04"],
			// Middle are headless. Not yet added.
			M4: [ , "26500", "27408", "2780e", "27b0e", , , "27e0e", "28108", "28208", "28308"],
			M5: [ , "2050", "2080", "2070", "20b0", "2210", "2250", "20e0", "2110", "2160", "21b0"],
			M6: [ , "2fb0", "2fc0", "2fd0", "2f80", "2fa0", "2fa2", "2f71", "2f73", "2f90", "2f92"],
			M7: [],
			// Five missing. Confusing symbols.
			M8: [ , "2880a", "2920a", "29906", "2950a", "29f0c" ],
			M9: [ , "2b70", "2b90", "2bd0", "2c30", "2c10", "2c105", "2b705", "2b905", "2bd05", "2c305" ],
			MA: [ , "2c60", "2c80", "2cc0", "2d20", "2d00", "2d005", "2c605", "2c805", "2cc05", "2d205" ],
			// Incomplete.
			MB: [ , "2d50", "2da0", "2db0", "2df0", "2dd08" ],
			// Confusing. Seem to be all rotations of earlier symbols.
			// MC: [ ]
			H1: [ , "30a0", "30a1", "30c0", "30c1", "3110", "3130", "30e0", "30f0", "30d0", "3100" ],
			// Incomplete. Can't figure out what the last one is.
			H2: [ , "3210", "3240", "3140", "3160", "3310", "3330", "31a0", "3190", "31b0" ],
			// Incomplete. Can't figure out what the last one is.
			H3: [ , "32a0", "32b0", "32c0", "35e04", "3350", "3391", "32d0", "32d1", "32f1" ],
			H4: [ , "33b0", "33e0", "3410", "3440", "3450", "3460", "3470", "34a0", "34d0", "3530" ],
			H5: [ , "3580", "3581", "3570", "3571", "3500", "3510", "3560", "3561", "3550", "3540" ],
			// Incomplete. Can't figure out what the last one is.
			H6: [ , "3610", "3630", "3650", "3671", "3670", "35f0", "3590", "35a0", "35c0" ],
			H7: [ , "30004", "2ff0", , "3301", "3680", "3690", "36a0", , "36c0", "36b0" ]
		};
		
		function useSymb( ind ) {
			return function ( x, y, keyName ) {
				if ( this.alt[ 0 ] === 0 ) {
					this.alt[ 0 ] = keyName;
					this.face = 0;
				} else if ( this.alt[ 1 ] === 0 ) {
					if ( symbMaps[ this.alt[ 0 ] ][ ind ] ) {
						this.alt[ 1 ] = ind + "";
					} else {
						return "";
					}
				} else {
					if ( ind < 5 || ( ind > 6 && ind < 11 ) ) {
						this.rotation = ind > 6 ? ind - 3 : ind - 1;
						this.type = symbolTypes.HAND;
						if ( !this.fake ) {
							this.buildElement();
						}
					} else {
						return "";
					}
				}
			};
		}
		
		var layout = {
			// Label to appear in the layouts section of the settings menu.
			label: "SignWriter",
			// Each position in this variable should have a string corresponding
			// to the name of a function in baseKeyboardActions (below), which 
			// is called when the corresponding key is pressed.
			baseKeyboardLayout :
				( "?  H1 H2 H3 H4 H5 H6 H7 ?  ?  ?  ?  ?     " +
				  "    M1 M2 M3 M4 M5 M6 ?  M8 M9 MA MB ?  ? " +
				  "     K1 K2 K3 K4 K5 K6 K7 K8 K9 KA KB     " +
				  "      ?  KC KD KE KF Fa C  Fl E   R       " ).split(/\s+/g),
			// Also, I still need to add a way to override updateImage defaults.
			//
			// These are the actual actions that are taken when a user presses
			// a key, as per baseKeyboardLayout. 
			baseKeyboardActions: {
				"K1" : useSymb( 1 ),
				"K2" : useSymb( 2 ),
				"K3" : useSymb( 3 ),
				"K4" : useSymb( 4 ),
				"K5" : useSymb( 5 ),
				"K6" : useSymb( 6 ),
				"K7" : useSymb( 7 ),
				"K8" : useSymb( 8 ),
				"K9" : useSymb( 9 ),
				"KA" : useSymb( 10 ),
				"KB" : useSymb( 11 ),
				"KC" : useSymb( 12 ),
				"KD" : useSymb( 13 ),
				"KE" : useSymb( 14 ),
				"KF" : useSymb( 15 ),
				"M1" : useSymb( 20 ),
				"M2" : useSymb( 21 ),
				"M3" : useSymb( 22 ),
				"M4" : useSymb( 23 ),
				"M5" : useSymb( 24 ),
				"M6" : useSymb( 25 ),
				"M8" : useSymb( 27 ),
				"M9" : useSymb( 28 ),
				"MA" : useSymb( 29 ),
				"MB" : useSymb( 30 ),
				"H1" : useSymb( 40 ),
				"H2" : useSymb( 41 ),
				"H3" : useSymb( 42 ),
				"H4" : useSymb( 43 ),
				"H5" : useSymb( 44 ),
				"H6" : useSymb( 45 ),
				"H7" : useSymb( 46 ),
				"Fa" : function () { // Face / Plane
					// Cycles through different planes and faces.
					if ( this.alt[ 1 ] !== 0 ) {
						this.face = ( this.face || ( symbMaps[ this.alt[ 0 ] ][ this.alt[ 1 ] ][ 3 ] + 1 ) ) % 6 + 1;
					} else {
						return "";
					}
				},
				"C" : function () { // Cursor.
					if ( !this.fake ) {
						var oldSymbol, newSymbol;
						
						if ( this.type !== symbolTypes.NONE ) {
							oldSymbol = this;
							this.blur();
							newSymbol = 
								new SignSymbol( null, activeSign );
							activeSign.symbols.push( newSymbol );
							newSymbol.focus();
							newSymbol.alt[ 5 ] = 6;
						} else {
							newSymbol = this;
							oldSymbol = activeSign.symbols[ 
								activeSign.symbols.indexOf( newSymbol ) - 1
							];
							if ( oldSymbol ) {
								newSymbol.alt[ 5 ] = (
									newSymbol.alt[ 5 ] + 
									( keyboard.shiftKey ? 7 : 1 )
								) % 8;
							}
						}
						
						if ( oldSymbol ) {
							// Positioning
							// Positioning, copied from baselayout
							var q = { 0 : 0, 1 : 0, 2 : 0.5, 3 : 1, 4 : 1, 5 : 1, 6 : 0.5, 7 : 0 };
							
							// TODO: Use actual dimensions of target symbols.
							// This isn't possible with the current positioning
							// system, I think. Unless each keypress also checks
							// to update the position?
							newSymbol.Y = oldSymbol.Y + Math.floor(
								( oldSymbol.height + 5 ) * q[ newSymbol.alt[ 5 ] ] - 
								20 * q[ ( newSymbol.alt[ 5 ] + 4 ) % 8 ]
							);
							newSymbol.X = oldSymbol.X + Math.floor(
								( oldSymbol.width + 5 ) * q[ ( newSymbol.alt[ 5 ] + 6 ) % 8 ] - 
								20 * q[ ( newSymbol.alt[ 5 ] + 2 ) % 8 ]
							);
						}
					} else {
						return "";
					}
				},
				"Fl" : function () { // Flip
					this.alt[ 3 ] ^= 1;
				},
				"E" : function () { // Enlarge
					return "";
				},
				"R" : function () { // Rotate
					this.rotation = ( 
						this.rotation + 8 + ( keyboard.shiftKey ? -1 : 1 ) 
					) % 8;
				}
			},
			// If a map other than baseKeyboardLayout should sometimes be used,
			// this can return a different one based on the symbol.
			modeMap: function ( symbol ) {
				return layout.baseKeyboardLayout;
			},
			// Keys that don't have images.
			imagelessKeys: [],
			// Keys that don't trigger a keyboard update when pressed.
			suppressUpdateKeys: [],
			// Just in case you want to toss some plain text on top of a key.
			keyTexts: {},
			/**
			 * Takes a SignSymbol (mapped to "this") and finds the correct 
			 * symbolCode, based on its internal content props and such.
			 * @return {String} symbolCode
			 */
			symbolFSW: function () {
				var result;
				if ( this.alt[ 0 ] !== 0 ) {
					result = symbMaps[ this.alt[ 0 ] ];
					if ( result ) {
						result = result[ this.alt[ 1 ] || 1 ];
						if ( this.face !== 0 ) {
							result = result.substr( 0, 3 ) + ( this.face - 1 );
						}
						result += ( this.rotation + this.alt[ 3 ] * 8 ).toString( 16 );
						return result;
					} else {
						return "";
					}
				}
			},
			/*
			 * This should set all of the various internal properties of the
			 * symbol so that they match the symbolCode (FSW) given. If that's
			 * not possible for whatever reason, return false so that the system
			 * will make the symbol a basic "UNSPECIFIED"-type symbol that is
			 * visible and shows up in the FSW but isn't easily modifiable.
			 */
			symbolFromFSW: function ( symbolCode, symbol ) {
				// TODO
				if ( symbolCode === "10020" ) {
					symbol.type = symbolTypes.HAND;
					symbol.alt[ 0 ] = 1;
					return symbol;
				} else {
					return false;
				}
			},
			hooks: {}
		};
		
		return layout;
	}
	
	// Empty skeleton layout, for development purposes.
	function SkeletonLayout() {
		var layout = {
			// Label to appear in the layouts section of the settings menu.
			label: "...",
			// Each position in this variable should have a string corresponding
			// to the name of a function in baseKeyboardActions (below), which 
			// is called when the corresponding key is pressed.
			baseKeyboardLayout :
				( "?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?     " +
				  "    ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ?  ? " +
				  "     K1 ?  ?  ?  ?  ?  ?  ?  ?  ?  ?      " +
				  "      ?  ?  ?  ?  ?  ?  ?  ?  ?   ?       " ).split(/\s+/g),
			// Also, I still need to add a way to override updateImage defaults.
			//
			// These are the actual actions that are taken when a user presses
			// a key, as per baseKeyboardLayout. 
			baseKeyboardActions: {
				"K1" : function () {
					this.type = symbolTypes.HAND;
					this.alt[ 0 ] = 1;
					if ( !this.fake ) {
						this.buildElement();
					}
				}
			},
			// If a map other than baseKeyboardLayout should sometimes be used,
			// this can return a different one based on the symbol.
			modeMap: function ( symbol ) {
				return layout.baseKeyboardLayout;
			},
			// Keys that don't have images.
			imagelessKeys: [],
			// Keys that don't trigger a keyboard update when pressed.
			suppressUpdateKeys: [],
			// Just in case you want to toss some plain text on top of a key.
			keyTexts: {
				"K1" : "Not yet available."
			},
			/**
			 * Takes a SignSymbol (mapped to "this") and finds the correct 
			 * symbolCode, based on its internal content props and such.
			 * @return {String} symbolCode
			 */
			symbolFSW: function () {
				if ( this.alt[ 0 ] === 1 ) {
					return "10020";
				}
			},
			/*
			 * This should set all of the various internal properties of the
			 * symbol so that they match the symbolCode (FSW) given. If that's
			 * not possible for whatever reason, return false so that the system
			 * will make the symbol a basic "UNSPECIFIED"-type symbol that is
			 * visible and shows up in the FSW but isn't easily modifiable.
			 */
			symbolFromFSW: function ( symbolCode, symbol ) {
				if ( symbolCode === "10020" ) {
					symbol.type = symbolTypes.HAND;
					symbol.alt[ 0 ] = 1;
					return symbol;
				} else {
					return false;
				}
			},
			hooks: {}
		};
		
		return layout;
	}
	
	var layouts = [ {
		label : "Default",
		init: BaseLayout
	}, {
		label: "SignWriter",
		init: DOSLayout
	} ];
	
	// Do a bit of processing on the layout.
	function getLayout( targetLayout ) {
		// TODO: Set up something more sophisticated than this.
		// var layout = layouts[ index || 0 ]();
		var layout;
		if ( typeof targetLayout === "function" ) {
			layout = targetLayout();
		} else {
			for ( var i = 0; i < layouts.length; i++ ) {
				if ( layouts[ i ].label === targetLayout ) {
					layout = layouts[ i ].init();
				}
			}
			if ( !layout ) {
				layout = layouts[ 0 ].init();
			}
		}
		// Wait, this is seriously confusing. 
		// Setting layout as a local property separate from layout as a 
		// less-local property is a bad idea. TODO: Fix/Rename.
		
		// Convert imagelessKeys and suppressUpdateKeys from arrays to simpler
		// objects.
		// TODO: Eliminate duplication.
		layout.imagelessKeysObject = {};
		for ( var i = 0; i < layout.imagelessKeys.length; i++ ) {
			layout.imagelessKeysObject[ layout.imagelessKeys[ i ] ] = true;
		}
		
		layout.suppressUpdateKeysObject = {};
		for ( var i = 0; i < layout.suppressUpdateKeys.length; i++ ) {
			layout.suppressUpdateKeysObject[ layout.suppressUpdateKeys[ i ] ] = true;
		}
		
		return layout;
	}
	
	/**
	 * @param {Function} targetLayout Function that generates a layout to be 
	 *  switched to.
	 */
	function changeLayout( targetLayout ) {
		layout = getLayout( targetLayout );
		for ( var i = 0; i < signAreas.length; i++ ) {
			for ( var ii = 0; ii < signAreas[ i ].signs.length; ii++ ) {
				var iii = 0, 
					symbols = signAreas[ i ].signs[ ii ].symbols,
					symbol;
				for ( ; iii < symbols.length; iii++ ) {
					symbol = symbols[ iii ];
					if ( symbol.type !== symbolTypes.NONE ) {
						symbol.useSymbolFSW();
					}
				}
			}
		}
		// TODO: Redo the keyboard.
		keyboard.blank();
		keyboard.rewrite();
		keyboard.update();
	}
	
	
	// ** POLYFILLS **
	
	var requestAnimationFrame = window.requestAnimationFrame ||
		window.mozRequestAnimationFrame || 
		window.webkitRequestAnimationFrame ||
		function ( callback ) {
			return setTimeout( callback, 16 );
		},
	cancelAnimationFrame = window.cancelAnimationFrame ||
		window.mozCancelAnimationFrame ||
		window.webkitCancelAnimationFrame ||
		function ( timer ) {
			clearTimeout( timer );
		};
	
	if ( !Date.now ) {
		Date.now = function () {
			return new Date().getTime();
		};
	}
	
	// These two are incomplete.
	if( ![].indexOf ) { // IE8< doesn't support indexOf...
		Array.prototype.indexOf = function ( target ) {
			for ( var i = 0, l = this.length; i < l; i++ ) {
				if ( this[i] === target ) {
					return i;
				}
			}
			return -1;
		};
	}
	
	if( ![].lastIndexOf ) { 
		Array.prototype.lastIndexOf = function ( target ) {
			for ( var i = this.length; i >= 0; i-- ) {
				if ( this[i] === target ) {
					return i;
				}
			}
			return -1;
		};
	}
	
	var addEventListener = function ( element, type, callback, capture ) {
		if ( element.addEventListener ) {
			element.addEventListener( type, callback, capture );
		} else {
			element.attachEvent( "on" + type, callback );
			// Currently breaks IE8 on SA, due to lack of this.
			// TODO: Use the following instead, once a replacement for 
			// detachEvent is ready.
			/*
			element.attachEvent( "on" + type, function ( e ) {
				callback.call( element, e );
			} );
			*/
		}
	},
	removeEventListener = function ( element, type, callback, capture ) {
		if ( element.removeEventListener ) {
			element.removeEventListener( type, callback, capture );
		} else {
			element.detachEvent( "on" + type, callback );
		}
	};
	
	// ** SIGN EDITING FIELD **
	// SignAreas contain Signs which contain SignSymbols
	// SignAreas can also contain NonSignTexts
	/**
	 * A Sign-editing field, usually associated with an existing text box.
	 * 
	 * @constructor
	 * @param {HTMLElement} [textbox] A textarea, input, or 
	 *  contenteditable element to be associated with the SignArea. This will 
	 *  have its content and selection position synchronized with the SignArea.
	 * @cfg {HTMLElement} [element]
	 * @cfg {Sign[]|NonSignText[]} [signs] List of all Signs and NonSignTexts
	 *  within the SignArea.
	 */
	function SignArea( textBox ) {
		// TODO: Figure out some way of mimicking shadow dom so that these CSS
		// (and maybe JS) don't interfere with the contents.
		// Actually, use shadow DOM itself where possible to avoid the whole
		// MutationObservers thing. (Does that actually work?)
		var thiz = this;
		signAreas.push( this );
		
		// TODO: Build a generic "start up general things" function.
		fontHandler.init();
		
		var container = 
			this.container = document.createElement( "div" );
		// I don't like the name of this property. TODO: Rename.
		this.element = 
			container.appendChild( document.createElement( "div" ) );
		var outerContainer =
			this.outerContainer = document.createElement( "span" );
		this.textBox = textBox;
		// Currently unused.
		this.lastCaret = 0;
		this.lastActive;
		this.signs = [];
		this.signText = "";
		this.size = 1;
		
		// Prevent content from being hidden by scroll bars. Assume Windows
		// scroll bar size for now. TODO: Detect scroll bar size.
		this.element.style.paddingBottom = "17px";
		
		if ( config.ce ) {
			this.element.contentEditable = true;
			
			// Add a whole lot of event listeners: 
			// onselect doesn't work for contenteditable elements.
			// ISSUE: If a user straight-out deletes a Sign's element by means
			// of right-click>delete, there's no way to detect this, afaict.
			// For that matter, dragging a selection that includes a Sign also
			// really breaks things.
			addEventListener( this.element, "click", function () {
				caret.check();
			}, true );
			addEventListener( this.element, "blur", function ( e ) {
				// Issue: This should *not* blur if one is clicking anywhere 
				// inside the keyboard, whether a key or the settings box.
				// console.log( "BLURRING signArea element", 
				//	(caret.busy || caret.refocusRange) ? "(but not really)" : "(really)", caret.busy );
				
				// IE Issue: Blur seems to have a delay, so caret.busy isn't 
				// working. Possible solution: Check with getSelection before
				// running the blur stuff. Could be expensive, though.
				// Another problem: getSelection doesn't change until after
				// blur runs in Chrome. Need a solution that doesn't break
				// either browser.
				
				// Actually, IE does not seem to have a delay. Strange. I guess
				// something is refocusing? Does that even make sense?
				
				// No, wait, it actually does have a delay. Compare the results:
				// wpTextbox1=$("#wpTextbox1")[0];wpTextbox1.onblur=function(){console.log(98)};
				//wpTextbox1.onclick=function(){setTimeout(function(){wpTextbox1.blur();console.log(2);},500)}
				
				// Unfortunately, focusNode does not update quickly enough.
				// document.activeElement might, though...
				if ( thiz === signArea && !caret.busy && !caret.refocusRange ) { 
					// TODO: Deal with this:
					// Uncommented, it breaks Chrome. Commented, it breaks IE.
					/*
					if ( caret.check() !== false ) {
						// Note that .check will also move the cursor back when
						// necessary.
						return;
					}
					*/
					// Maybe? TODO: Actually think through this. Before saving.
					if ( document.activeElement === thiz.element
						|| document.activeElement === activeSign.ce
						// For IE, which has a weird idea of "activeElement".
						|| document.activeElement === activeSign.element
					) {
						return;
					}
					
					if ( delegateEvent( e ) === false ) {
						// Activate change event, just in case.
						// console.log( "CHANGE EVENT" );
						var event = document.createEvent('Event');
						event.initEvent( "change", true, true );
						thiz.element.dispatchEvent( event );
						// Fake event. Don't loop.
						return false;
					}
					thiz.blur();
				}
				
				if ( caret.busy && caret.refocusRange ) {
					console.error( "111: busy and refocusRange are both on" );
				}
				
				if ( caret.refocusRange ) {
					caret.selectRange( caret.refocusRange );
					caret.refocusRange.detach();
					caret.refocusRange = false;
				}
				
			}, true );
			addEventListener( this.element, "mousedown", function () {
				// Don't blur.
				caret.busy = true;
				// Wait, what? Does this ever get undone? 
				// Well, the normal caret things still seem to work, so I guess
				// the answer is yes? Maybe caret.check() in the click handler?
				
				// This needs to have some stuff for selection-handling.
				// If the user mousedowns a Sign or NST, stick on a class and 
				// set selectionStart to there, then prep for noticing 
				// mousemove events, and the eventual mouseup to remove 
				// the mousemove tracking.
				// Also, what if shift is pressed? Need to fill out the 
				// selection.
			});
			addEventListener( this.element, "mouseup", function ( e ) {
				// TODO: Fix the issue with this blocking clicking on a chunk
				// of selected text.
				caret.textBoxSync( true );
				caret.check();
			}, true );
			addEventListener( this.element, "focus", function ( e ) {
				// console.log( "FOCUSING signArea.element. caret.busy=", caret.busy );
				if ( !caret.busy ) {
					if ( delegateEvent( e ) === false ) {
						return false;
					}
					if ( thiz !== signArea ) { 
						thiz.focus();
					}
				}
			}, true );
			addEventListener( this.element, "copy", function ( e ) {
				return caret.copyFSW( e );
			}, true );
			addEventListener( this.element, "cut", function ( e ) {
				// return caret.copyFSW( e );
				// This doesn't even do anything.
				return false;
			}, true );
			addEventListener( this.element, "paste", function ( e ) {
				return caret.pasteFSW( e );
			}, true );
			// Note: This is not supported by many browsers.
			addEventListener( this.element, "input", function ( e ) {
				return;
				if ( config.ce && activeSign && activeSign.isSign ) {
					if ( activeSign.ce.firstChild ) {
						// This is quite possibly slower than ideal. ACE uses
						// textnode.value = (placeholder) instead.
						// This gets called on every keypress. 
						// Need to consider performance. TODO.
						activeSign.ce.removeChild( activeSign.ce.firstChild );
					}
				}
			}, true );
		}
		
		if ( textBox ) {
			// TODO: Deal with the broken width and height of input boxes in a 
			// horizontal-tb environment in nonsupporting browsers.
			
			// POSITIONING
			// This doesn't work well in all situations.
			// Options:
			// 1. Add SA before TB, position: absolute and matched dimensions.
			//    Doesn't work if other elements float, ex. in link dialogue.
			//    (Previously in use.)
			// 2. Add SA after TB, with negative top-margin matching height.
			//    Doesn't work where TB has margin-bottom, ex. in summary field.
			// 3. Add SA and TB to new div in place. Absolute SA before TB, or
			//    TB before SA. (Current.)
			
			// NOTE: There are still problems with placing SAs in elements that
			// have overflow: hidden. One solution that works in certain 
			// browsers is to use position: fixed; with no top or left set, and
			// use margin to position. absolute does not "break out" of the 
			// element for overflow purposes. The position: fixed; method does
			// not work in certain older IEs (untested). I haven't checked to
			// see whether this freezes the scroll position of the element.
			// ...Argh, it does. Nvm, can't use this.
			
			if ( textBox.nodeName === "TEXTAREA" || textBox.nodeName === "INPUT" ) {
				this.textBoxType = textBox.nodeName;
			} else if ( textBox.contenteditable === "true" ) {
				this.textBoxType = "CE";
			} else {
				this.textBoxType = "NONE";
			}
			
			this.styleDOM();
			
			// Mins don't work, since blocks stretch out then.
			// TODO: Fix searchBox's scroll, without breaking other things.
			
			// Are possible %-paddings important? 
			
			/*
			// TODO: Set title.
			// Problem is, the title attribute is removed so that it doesn't go
			// over the pseudo-titleboxes generated by signwriting_viewer.js.
			// How to access it? Hm...
			if ( this.textBox.title ) {
				this.container.title = this.textBox.title;
			}
			*/
			
			// ISSUE: A shrunk-width summary box doesn't size the textbox 
			// correctly. Maybe change setSize and shrink to also set textbox's
			// min-height/widths.
			// Also, maybe TODO: floats.
			// Definitely TODO: Test on a wider variety of text boxes.
			
			if ( textBox.getAttribute( "disabled" ) || textBox.getAttribute( "readonly" ) ) {
				// textBox is disabled. Don't allow editing of SW content.
			}
			
			// Unfortunately, onchange does not work.
			/*
			addEventListener( textBox, "change", function () {
				console.log( "TEXTBOX Content changed" );
				thiz.handleTextBoxChange();
			});
			*/
			// Consider adding an onresize event listener to the textBox.
			// ...does onresize only work for window?
			// Consider adding a languagechange event listener.
		} else {
			document.body.insertBefore( container, document.body.firstChild );
		}
		
		container.className = "SWKB-SignArea";
		
		// Maybe later v
		// this.element = this.element.createShadowRoot();
		
		this.focus();
		
		this.addSign( 0 );
		this.signs[ 0 ].focus();
		this.history = new SignHistory();
		// this.focus();
		
		// If we focused before the Sign was created, we need to update the 
		// keyboard again after. 
		// If we only focused after, the blur won't work because activeSign will
		// already be the new sign.
		
		if ( textBox && textBox.value ) {
			interpretFSW( textBox.value );
		}
		
		//this.history = new SignHistory();
		
		addEventListener( container, "click", function () {
			if ( signArea !== thiz ) {
				thiz.focus();
			}
		}, true );
		
		if ( config.ce ) {
			// Sigh
			// Note: This line was added when I didn't remember what the earlier
			// stuff was about.
			this.signs[ 0 ].focus();
		}
		
		keyboard.update();
		
	}
	
	SignArea.prototype = {
		styleDOM: ( function () {
			
			// TODO: Don't repeatedly call getComputedStyle. Call it once and
			// reuse the whole object as necessary.
			
			function getStyle( elem, style ) {
				return ( window.getComputedStyle ? 
					getComputedStyle( elem, null ) : 
					elem.currentStyle
				)[ style ];
			}
			
			function getAllStyles( elem ) {
				return ( window.getComputedStyle ? 
					getComputedStyle( elem, null ) : 
					elem.currentStyle
				);
			}
					
			function getPixels( x ) {
				return parseFloat( x.replace( /px$/, '' ) );
			}
			
			return function styleDOM() {
				var originalDisplay;
				
				var container = this.container,
					containerStyle = container.style,
					outerContainer = this.outerContainer,
					outerContainerStyle = outerContainer.style,
					textBox = this.textBox,
					textBoxStyle = textBox.style;
			
				// Lots of sizing and positioning.
				// TODO: Don't misposition the text box. Check with .disable().
				// 
				// Sizing notes: 
				// * getComputedStyle on height or width will always return simple
				//   pixel amounts, unless the element is hidden.
				// * height/width: inherit will duplicate the dimensions of the
				//   parent element, even if it's a percent value. If the parent's
				//   width is 50%, the child's will be 25% of grandparent element.
				// * % values do not account for padding or margins. An element with
				//   width/height: 100% and a border will overflow the parent. 
				// * Remember, container is absolute positioned, so % dimensions 
				//   mean relative to outerContainer.
				// * Using box-sizing: border-box messes up getComputedStyle for 
				//   logical-height in all major browsers, I think.
				// * The edit box uses border-box by itself. This needs to be 
				//   accounted for.
				
				var allTextBoxStyles = getAllStyles( textBox );
				
				if ( config.scaleSigns ) {
					this.size = 
						allTextBoxStyles.fontSize.replace( /px$/, '' ) / 30;
					
			
					if ( this.size !== 1 ) {
						this.element.style.fontSize = this.size * 100 + "%";
					}
					// this.element.style.fontSize = this.size * 100 + "%";
				} else {
					// Breaks in IE8, due to textBox being undefined due to
					// attachEvent not setting "this".
					this.element.style.fontSize = allTextBoxStyles.fontSize;
				}
				
				var originalDisplay = allTextBoxStyles.display;
				outerContainerStyle.display = originalDisplay === "inline" ?
					"inline-block" : 
					originalDisplay;
				outerContainerStyle.position = 
					allTextBoxStyles.position === "static" ?
						"relative" : 
						allTextBoxStyles.position;
				
				// Is the originalWidth stuff still necessary? Maybe remove it?
				// Still used by .hide(). Don't know if it's necessary/makes sense 
				// there.
				this.originalWidth = allTextBoxStyles.width;
				this.originalHeight = allTextBoxStyles.height;
				this.originalOWidth = textBox.offsetWidth;
				this.originalOHeight = textBox.offsetHeight;
				containerStyle.overflow = "auto";
				// Hide the textbox so we can get non-pixel values for height/width.
				// Odd CSS quirk.
				textBoxStyle.display = "none";
				this.originalHWidth = getStyle( textBox, "width" ) || this.originalWidth;
				this.originalHHeight = getStyle( textBox, "height" ) || this.originalWidth;
				this.originalWidth = this.originalHWidth !== "auto" ? this.originalHWidth : this.originalWidth;
				this.originalHeight = this.originalHHeight !== "auto" ? this.originalHHeight : this.originalHeight;
				outerContainerStyle.margin = allTextBoxStyles.margin;
				// Need to duplicate things precisely...
				outerContainerStyle.maxHeight = allTextBoxStyles.maxHeight;
				outerContainerStyle.maxWidth = allTextBoxStyles.maxWidth;
				outerContainerStyle.resize = allTextBoxStyles.resize;
				// IE can't handle border-width as one property. border-top-width
				// and such do work, though. TODO: Change it.
				//container.style.margin = getStyle( textBox, "border-width" );
				containerStyle.marginTop = allTextBoxStyles[ "border-top-width" ];
				containerStyle.marginRight = allTextBoxStyles[ "border-right-width" ];
				containerStyle.marginBottom = allTextBoxStyles[ "border-bottom-width" ];
				containerStyle.marginLeft = allTextBoxStyles[ "border-left-width" ];
				
				// The stylesheet isn't usable here for the container because of 
				// autostart, and for the textbox because it doesn't really have a
				// convenient selector if contenteditables are included.
				
				containerStyle.width  = textBoxStyle.width  = 
				containerStyle.height = textBoxStyle.height = "100%";
				
				outerContainerStyle.width = this.originalWidth;
				outerContainerStyle.height = this.originalHeight;
				
				textBoxStyle.display = originalDisplay;
				textBoxStyle.margin = 0;
				
				this.setSize();
				
				var zIndex = +allTextBoxStyles[ "z-index" ];
				if ( zIndex > 9 ) {
					containerStyle.zIndex = zIndex + 1;
				}
				
				
				textBox.parentNode.insertBefore( outerContainer, textBox );
				outerContainer.appendChild( container );
				outerContainer.appendChild( textBox );
				
				containerStyle.padding = allTextBoxStyles.padding;
				
				// TODO (maybe): Use prefixes for box-sizing.
				if ( ( containerStyle.boxSizing = allTextBoxStyles[ "box-sizing" ] )  !== "border-box" ) {
					
					var y = getPixels( allTextBoxStyles.paddingTop ) +
						getPixels( allTextBoxStyles.paddingBottom ) +
						getPixels( allTextBoxStyles.borderTopWidth ) +
						getPixels( allTextBoxStyles.borderBottomWidth );
					
					var x = getPixels( allTextBoxStyles.paddingLeft ) +
						getPixels( allTextBoxStyles.paddingRight ) +
						getPixels( allTextBoxStyles.borderLeftWidth ) +
						getPixels( allTextBoxStyles.borderRightWidth );
					
					outerContainerStyle.top = "-" + y/2 + "px";
					outerContainerStyle.left = "-" + x/2 + "px";
					outerContainerStyle.border = "solid transparent";
					outerContainerStyle.borderWidth = y/2 + "px " + x/2 + "px";
				}
			}
		})(),
		/**
		 * Focuses the SignArea.
		 */
		focus: function () {
			if ( this === signArea ) {
				// ... Should probably do something.
				// Oh, also update caret position stuff.
				//
				// Wait a second, this is almost always caused by updateCaret 
				// itself. Need some way to deal with this.
			} else {
				// console.log( "Focusing SignArea ", this );
				signArea && signArea.blur();
				signArea = this;
				signListElem = this.element;
				if ( this.shrunk ) {
					this.unshrink();
				} else {
					this.setSize();
				}
				if ( this.signs.length ) {
					( this.lastActive || this.signs[ 0 ] ).focus();
					keyboard.update();
				}
			}
		},
		/**
		 * Removes the focus from the SignArea
		 */
		blur: function () {
			
			// console.log( "SignArea blur: Blurring Sign", activeSign );
			if ( activeSign ) {
				// Cleanup the last sign.
				if ( activeSign.isSign ) {
					activeSign.centerSign();
				}
				this.lastActive = activeSign;
				activeSign.blur();
			}
			if ( this.textBox ) {
				this.updateTextboxContent();
				this.shrink();
			}
			this.coords = undefined;
			
			signArea = 
				signListElem = undefined;
			keyboard.hide();
		},
		/**
		 * @return {string} Text and FSW content of the SignArea.
		 */
		toString: function () {
			var result = this.signs.join( " " );
			
			result = fswParser.processMixedText( result );
			
			// cache
			this.signText = result;
			
			return result;
		},
		// Legacy version of .addSign().
		// TODO: Remove this.
		/**
		 * Adds a Sign to the SignArea.
		 * @param {Sign|NonSignText} sign The Sign to be added.
		 * @param {number} index
		 */
		addSignLegacy: function ( sign, index ) {
			// TODO: Allow for unspecified index.
			// Should that add to the end, or after activeSign?
			
			
			sign.signArea = this;
			sign.setDomSize && sign.setDomSize();
			// First, add the element to the DOM.
			try {
				if ( index >= this.signs.length ) {
					this.element.appendChild( sign.element );
				} else {
					this.element.insertBefore( sign.element, this.signs[ index ].element );
				}
			} catch( e ) {
				console.error( e, sign, index, this.signs, this.signs[ index ] );
			}
			// Add the sign to this.signs.
			// (splice can handle indexes higher than length, without throwing
			// in undefines or anything else scary.)
			this.signs.splice( index, 0, sign );
		},
		/**
		 * Adds a Sign to the SignArea.
		 * @param {number} index
		 * @param {number} [text] The Formal SignWriting (FSW) to be
		 *  the starting content of the sign.
		 * @return {Sign}
		 */
		addSign: function ( index, text ) {
			return this.addText( Sign, index, text );
		},
		/**
		 * Adds a NonSignText (an area with plain non-sign language text) to
		 * the SignArea.
		 * @param {number} index
		 * @param {number} [text] The Formal SignWriting (FSW) to be
		 *  the starting content of the sign.
		 * @return {NonSignText}
		 */
		addNonSignText: function ( index, text ) {
			return this.addText( NonSignText, index, text )
		},
		/**
		 * Adds a text unit (Sign or NonSignText) to the SignArea.
		 * @param {function} TextType Sign
		 */
		addText: function ( TextType, index, text ) {
			// TODO: Allow for unspecified index.
			// Should that add to the end, or after activeSign?
			
			var sign = new TextType( this, text );
			//sign.signArea = this;
			sign.setDomSize && sign.setDomSize();
			// First, add the element to the DOM.
			try {
				if ( index >= this.signs.length ) {
					this.element.appendChild( sign.element );
				} else {
					this.element.insertBefore( sign.element, this.signs[ index ].element );
				}
			} catch( e ) {
				console.error( e, sign, index, this.signs, this.signs[ index ] );
			}
			// Add the sign to this.signs.
			// (splice can handle indexes higher than length, without throwing
			// in undefines or anything else scary.)
			this.signs.splice( index, 0, sign );
			return sign;
		},
		/**
		 * Remove a Sign from the SignArea.
		 * @param {Sign|NonSignText} sign The Sign to be removed.
		 */
		removeSign: function ( sign ) {
			var index = sign.index();
			if ( index !== -1 ) {
				this.element.removeChild( sign.element );
				this.signs.splice( index, 1 );
			} else {
				// Log something, maybe?
			}
		},
		/**
		 * Change the currently focused Sign or NonSignText to a different one.
		 * @param {number|Sign|NonSignText} targetSign
		 * @param {boolean} suppressCaret
		 */
		changeSign: function ( targetSign, suppressCaret ) { 
			// Switch the active sign one earlier or later.
			// TODO: Consider filling in with a new blank sign if past the 
			// beginning or end.
			var targetSign = typeof targetSign === "number" ?
					this.signs[ targetSign ] : 
					targetSign,
				previousSign = activeSign;
			if( targetSign && targetSign !== activeSign  ) {
				
				if ( previousSign.isSign ) {
					this.history.pushState(); // Probably?
				}
				// Didn't work as expected. TODO.
				
				previousSign.blur();
				// What did suppressCaret do originally? The argument doesn't
				// exist in Sign.focus.
				targetSign.focus( suppressCaret );
				keyboard.update();
				// Are there any situations where either doesn't get set?
				
				emit( 'changeSign', previousSign, targetSign );
			}
		},
		setSize: function () {
			// This is called by SignArea and unshrink.
			var textBox = this.textBox, 
				columnWidth = config.signWidth + 3,
				baseHeight = config.signAreaHeight;
			if ( textBox ) {
				if ( this.shrunk ) {
					// Don't have a minimum size for already shrunk SAs.
					columnWidth = baseHeight = 0;
				}
				
				this.textBox.style.minWidth = 
					this.outerContainer.style.minWidth  = 
						config.signWidth + 3 + "px";
				this.textBox.style.minHeight = 
					this.outerContainer.style.minHeight = 
						config.signAreaHeight + "px";
			}
		},
		shrink: function () {
			var columnWidth = config.signWidth + 3,
				baseHeight = config.signAreaHeight,
				containerStyle = this.container.style,
				originalOWidth = this.originalOWidth,
				originalOHeight = this.originalOHeight;
			
			// Not sure about the textBox.
			// TO CONSIDER: Instead of setting the styles here, maybe just apply
			// an "inactive" class to SA that suppresses mins.
			this.textBox.style.minWidth = 
				this.textBox.style.minHeight = 
				this.outerContainer.style.minWidth  = 
				this.outerContainer.style.minHeight = "";
			
			// Set to textbox's original offsetWidth (not style.width)
			if ( originalOWidth < columnWidth ) {
				// This is too much in cases when there's only NSTs.
				// Maybe just always set size to full column width when active?
				this.element.style.marginLeft = ( originalOWidth - columnWidth ) / 2 + "px";
				containerStyle.overflow = "hidden";
				this.shrunk = true;
			}
			if ( originalOHeight < baseHeight ) {
				//containerStyle.height = originalOHeight + "px";
				containerStyle.overflow = "hidden";
				this.shrunk = true;
			}
		},
		unshrink: function () {
			this.shrunk = false;
			this.setSize();
			this.element.style.marginLeft = "";
			this.container.style.overflow = "auto";
		},
		getCoords: function () {
			return this.coords || 
				( this.coords = this.container.getBoundingClientRect() );
		},
		updateFromDOM: function () {
			// console.log( "Run updateFromDOM" );
			var signs = this.signs, 
				element = this.element,
				// index = signs.indexOf( activeSign ),
				NST;
			for ( var i = signs.length; i--; ) {
				if ( signs[ i ].element.parentNode !== element ) {
					// TODO: Blur if active. Or something.
					if ( signs[ i ] === activeSign ) {
						// ...
						// ( signs[ i + 1 ] || signs[ i - 1 ] ).focus()
						// No, that makes too many redundant focuses when going
						// backwards...
					}
					
					if ( signs[ i ].element.parentNode ) {
						
					} else {
						// console.log( "Removing ", signs[ i ] );
						signs.splice( i, 1 );
						if ( signs[ i ] && !signs[ i ].isSign ) {
							NST = signs[ i ];
						}
					}
				} else {
					if ( NST ) {
						if ( signs[ i ] && !signs[ i ].isSign ) {
							signs[ i ].mergeWith( NST );
						}
						NST = undefined;
					}
				}
			}
			// If there are now two adjacent NSTs, merge them.
		},
		/**
		 * Synchronize the SignArea's associated text box's content to that
		 * of the SignArea.
		 */
		updateTextboxContent: function () {
			// Ideally, this would be called every time the signText was 
			// modified, such as in SignSymbol.updateSignText or Sign.toString,
			// but not multiple times when run multiple times at once, such as
			// in centerSign. 
			// Currently, uTBC is tied to keypress (followed by tBS), as well as
			// handleTextboxChange and blur. 
			// TODO: Deal with this.
			if ( this.textBox ) {
				var FSW = this.toString();
				this.textBox.value = FSW;
			}
		},
		/**
		 * Synchronize the content of the SignArea to that of its text box.
		 * (Not yet functional.)
		 */
		handleTextboxChange: function () {
			
			// TODO: Work with whatever the new caret position in the box is.
			// Maybe set up some function in caret object for this. Opposite of
			// textBoxSync function. Either focus the right Sign, or put the 
			// caret in the right location in the NST, including selection.
			
			// NOTE: This is not yet fully functional. 
			// (Still thoroughly borked as of 3/17.)
			
			console.log( "handleTextboxChange" );
			
			var oldText = this.toString(),
				newText = this.textBox.value,
				signs = this.signs,
				signsLength = signs.length,
				substring = newText;
			
			// This function is reasonably low priority for the near future.
			
			// First, check if there ary any changes at all.
			// Remember to make sure that this is never called by the script itself
			// changing some content in the textbox.
			
			// Next: Run through signs, looking for any changes.
			/*
			for ( var i = 0, ii = 0, signs = this.signs; i < signs.length; i++ ) {
				var signString = signs[ i ].toString(), 
					index = substring.indexOf( signString );
				if ( index === 0 ) {
					substring = substring.substr( signString.length );
				} else if ( index === -1 ) {
					// It's gone. I guess we try to search for the next one that's
					// still here? Maybe?
				} else {
					// Either we have a simple gap with stuff inside, or we lost it
					// and things are gonna get complicated.
				}
				// Avoid DOM modifications until we're done identifying the issues.
			}
			*/
			
			// ATTEMPT 2. Ignore above.
			// Argh, I completely forgot about the whole mixed text complications 
			// thing. Signs that follow certain symbols have the space removed. ...
			/*
			for ( var start = 0; start < signs.length; start++ ) {
				var signString = signs[ start ].toString(), 
					index = substring.indexOf( signString );
				if ( index === 0 || ( index === 1 && substring.charAt( 0 ) === " " ) ) {
					substring = substring.substr( signString.length + index );
				} else {
					break;
				}
			}
			for ( var end = signs.length - 1; end > 0; end-- ) {
				var signString = signs[ end ].toString(),
					// iirc, lastIndexOf isn't widely supported enough, yet.
					diffLength = substring.length - signString.length; 
				if ( substring.substr( diffLength ) === signString ) {
					substring = substring.substr( 0, diffLength );
				} else {
					break;
				}
			}
			*/
			
			
			// Do a binary search from both ends of the signList.
			// No wait, only from the start, then linear search forward.
			// Try to keep all (or as many as possible) of the Signs intact.
			// Hm, maybe I can have something like:
			// RegExp( signList.join( ".*" ).replace( /nonSignishstuff/g, '' ) ).test( textBox.value );
			/**
			 * Returns the lowest number for which compareFunc returns true.
			 * @param {Function} compareFunc A test that returns true if it is
			 *  an accepted value.
			 * @param {Number} min Lowest allowed value, inclusive.
			 * @param {Number} max Highest allowed value, inclusive.
			 */
			function binarySearch( compareFunc, min, max ) {
				if ( min >= max ) {
					return min;
				}
				var mid = Math.floor( ( min + max ) / 2 );
				if ( compareFunc( mid ) === true ) {
					return binarySearch( compareFunc, min, mid );
				} else {
					return binarySearch( compareFunc, mid + 1, max );
				}
			}
			
			// Find the earliest Sign incompatible with the new text.
			var divergence = binarySearch( function ( x ) {
				// Remember: slice is not inclusive by default.
				// Get text of all signs up to and including signs[ x ].
				var text = fswParser.processMixedText(
					signs.slice( 0, x + 1 ).join( " " )
				);
				return newText.indexOf( text ) !== 0;
			}, 0, signsLength - 1 );
			
			
			var eD = binarySearch( function ( x ) {
				// Get text of all signs after and including signs[ x ].
				var text = fswParser.processMixedText(
					signs.slice( x ).join( " " )
				);
				return newText.indexOf( text, newText.length - text.length ) !== -1;
			}, divergence, signsLength );
			
			
			console.log( "zzz = ", divergence, "eD = ", eD );
			// Okay, this is the wrong value. Maybe. Sometimes?
			
			/*
			for ( var end = pivot; end < signsLength - 1; end++ ) {
				var text = fswParser.processMixedText( signs.slice( end ).join( " " ) );
				if ( substring.indexOf( text, substring.length - text.length ) !== -1 ) {
					break;
				}
			}
			
			
			substring = substring.substr( fswParser.processMixedText( signs.slice( 0, pivot ).join(" ") ).length );
			*/
			//console.log( "%c" + substring, /*zzz,*/ "color: green;" );
			console.log( 456542, this.textBox.selectionStart );
			// This line should not be necessary. It's also getting very 
			// problematic.
			//signs[ divergence - 1 ] && signs[ divergence - 1 ].focus( true );
			//signs[ 0 ].focus();// temp
			activeSign.blur(); // maybe?
			console.log( 456543, this.textBox.selectionStart );
			
			var divSignString = ( signs[ divergence ] || "" ).toString();
			
			caret.busy = true;
			
			var oldLength = signs.length;
			
			for ( var i; signs[ divergence ] && signs.length > oldLength + divergence - eD; ) {
				// TODO: Only remove up to eD. Don't remove all following signs.
				console.log( "Removing: ", signs[ divergence ] );
				this.removeSign( signs[ divergence ] );
			}
			/*
			for ( ; signs[ divergence ]; ) {
				// TODO: Only remove up to eD. Don't remove all following signs.
				this.removeSign( signs[ divergence ] );
			}
			*/
			caret.busy = false;
			
			// Wait, wait, wait. This needs to make sure it doesn't add any
			// extraneous whitespace, as that will throw everything off. A lot.
			// divSignString is set to the wrong value. We want the *new* extra
			// stuff, not whatever was there before. Whatever.
			
			// ----
			/*
			// Testing area. Disable if not yet done.
			// Darn it, this actually is the problem causing all the issues.
			var w = signs.slice( 0, divergence ).join( " " ) + " " +
					divSignString;
			var x = fswParser.processMixedText( w );
			var y = w + newText.substr( x.length, 1 );
			var z = fswParser.processMixedText( y );
			var r = newText.substr( z.length );
			// Hm, complicated.
			*/
			// ----
			
			var preservedPreLength = 
				divergence === 0 ?
					this.toString() :
					fswParser.processMixedText( 
						signs.slice( 0, divergence ).join( " " ) + " " +
						divSignString
					).length - 
					divSignString.length, 
			preservedPostLength = 
				eD === oldLength ?
					0 :
					fswParser.processMixedText( 
						signs.slice( divergence ).join( " " )
					).length;
			
			substring = newText.substring( 
				preservedPreLength,
				newText.length - preservedPostLength
			);
			/*
			// If the above doesn't work, use this
			substring = newText.substring( 
				divergence === 0 ?
					this.toString() :
					fswParser.processMixedText( 
						signs.slice( 0, divergence ).join( " " ) + " " +
						divSignString
					).length - 
					divSignString.length
			);
			*/
			
			// Does something after this ruin the caret in IE?
			interpretFSW( substring, divergence );
			console.log( "ADDED TEXT: ", substring, "DIVERGENCE: ", divergence );
			//console.log( "NEWTEXT: ", newText );
			console.log( 45654, this.textBox.selectionStart, eD, preservedPostLength );
			
			
			var tB = this.textBox,
				selectionStart     = tB.selectionStart,
				selectionEnd       = tB.selectionEnd,
				selectionDirection = tB.selectionDirection;
			// try this before uTBC
			caret.reverseTextBoxSync( this, selectionStart, selectionEnd, selectionDirection );
			// This line here is tossing out our caret position.
			this.updateTextboxContent();
			console.log( 45655, this.textBox.selectionStart );
			
			// caret.reverseTextBoxSync( this, selectionStart, selectionEnd, selectionDirection );
			// TODO: SECOND HALF.
			
		},
		useFont: function () {
			var signs = this.signs,
				signElem,
				symbols,
				symbol, symbolStyle, newSymbolElem;
			
			keyboard.blank();
			
			config.useFont = true;
			
			for ( var i = 0; i < signs.length; i++ ) {
				symbols = signs[ i ].symbols;
				signElem = signs[ i ].element;
				for ( var ii = 0; ii < symbols.length; ii++ ) {
					symbol = symbols[ ii ];
					if ( symbol.type !== symbolTypes.NONE ) {
						symbolStyle = symbol.element.style;
						signElem.removeChild( symbol.element );
					}
					symbol.element = symbol._createDOM( true );
					symbol.addListeners();
					if ( symbol.type !== symbolTypes.NONE ) {
						symbol.element.style.top = symbolStyle.top;
						symbol.element.style.left = symbolStyle.left;
						signElem.appendChild( symbol.element );
						// This is extremely wasteful. TODO: Fix.
						symbol.updateImage();
					}
				}
			}
			activeSymbols[ false ].focus();
			activeSymbols[ true ].focus();
			
			keyboard.update();
			
		},
		show: function () {
			this.container.style.display = "block";
			this.setSize();
		},
		hide: function () {
			this.container.style.display = "none";
			if ( this.textBox ) {
				// Doesn't actually always make it go back to the right size.
				// The DOM changes can mess with things.
				this.textBox.style.width  = this.originalWidth;
			}
			// doesn't do anything
			// this.textBox.style.height = this.originalHeight;
		}
	};
	
	/**
	 * Record history states, for purposes of undoing and redoing.
	 * @constructor
	 */
	function SignHistory() {
		// Past states
		this.undos = [];
		// Undone states, as well as the *current* state, if undo was just 
		// called, without new actions having been taken since.
		// Example: If undo has been run twice, this will be:
		// [ undone state, more recently undone state, current state ]
		// and redo will add current state to undos, and then use the more
		// recently undone state for redoing while leaving it in the pile.
		this.redos = [];
	}
	
	SignHistory.prototype = {
		// Save current state in history for undo/redo purposes.
		// TODO: Lanes.
		// Perhaps this shouldn't assume that the SignArea is active.
		// TODO: Signspelling
		
		/**
		 * @return {Object} state Object containing data regarding the current
		 *  state of the content objects in use.
		 */
		getState: function () {
			return { 
				signs: cloneArray( signArea.signs ), 
				symbols: cloneArray( activeSign.symbols ), 
				activeSign: activeSign,
				activeSymbols: { 
					"false": activeSymbols[ false ], 
					"true":  activeSymbols[ true  ]
				},
				activeSymbolStates: {
					// Maybe instead just the signText could be copied?
					"false": activeSymbols[ false ].clone(), 
					"true" : activeSymbols[ true  ].clone()
				},
				lane: activeSign.lane
			};
		},
		/**
		 * Save the current state of the content objects. Use immediately before
		 * any changes.
		 */
		pushState: function () {
			try { 
				this.undos.push( this.getState() );
				
				while( this.redos.length > 0 ) {
					this.redos.pop();
				}
			} catch ( e ) {
				console.error( "SWKB: signHistory.pushState failed." );
			}
		},
		undo: function () {
			if ( this.undos.length > 0 ) {
				var newState = this.undos.pop();
				if ( this.redos.length === 0 ) {
					// Add the (old) current state to the start of redos.
					this.redos.push( this.getState() );
				}
				// Also add the new state to redos so that we don't have to call
				// getState an extra time if redo is called.
				this.redos.push( newState )
				this.updateToState( newState );
			}
		},
		/**
		 * Redo the most-recently undone change.
		 */
		redo: function () {
			if ( this.redos.length > 1 ) {
				this.undos.push( this.redos.pop() );
				this.updateToState( this.redos[ this.redos.length - 1 ] );
			}
		},
		/**
		 * Change state to match state given as argument.
		 * @param {Object} state
		 */
		updateToState: function updateToState( state ) {
			
			if ( activeSign === state.activeSign ) {
				// Haven't changed signs
				if ( 
					state.activeSymbols[ true ] === activeSymbols[ true ] &&
					state.activeSymbols[ false ] === activeSymbols[ false ]
				) {
					// Didn't change signs or symbols.
					for ( var i = 0, symbol, hasImage, hadImage; i < 2; i++ ) {
						i = !!i;
						symbol = activeSymbols[ i ];
						// Badly named...
						// Currently has an element in the DOM?
						hasImage = symbol.type !== symbolTypes.NONE;
						// SignSymbol that we're changing to has an element in the DOM?
						hadImage = state.activeSymbolStates[ i ].type !== symbolTypes.NONE;
						
						state.activeSymbolStates[ i ].clone( activeSymbols[ i ] );
						activeSymbols[ i ].updateImage();
						activeSymbols[ i ].updatePosition();
						
						if ( hasImage && !hadImage ) {
							activeSign.element.removeChild( symbol.element );
						} else if ( hadImage && !hasImage ) {
							symbol.buildElement( true );
						}
					}
				} else {
					// Changed symbols.
					var curSymbols = activeSign.symbols, newSymbols = state.symbols,
						changed; // SignSymbol removed or added, need updatePosition().
					
					activeSign.symbols = cloneArray( newSymbols );
					
					for ( var i = 0; i < curSymbols.length; i++ ) {
						if ( newSymbols.indexOf( curSymbols[ i ] ) === -1 ) {
							// Remove symbol
							curSymbols[ i ].type !== symbolTypes.NONE &&
							activeSign.element.removeChild(
								curSymbols[ i ].element 
							);
							changed = true;
						}
					}
					for ( var i = 0; i < newSymbols.length; i++ ) {
						if ( curSymbols.indexOf( newSymbols[ i ] ) === -1 ) {
							// Re-add symbol
							newSymbols[ i ].type !== symbolTypes.NONE &&
							// BUG: Using appendChild results in incorrect 
							// position for deleted elements not at the end.
							// TODO: Fix this thing.
							activeSign.element.appendChild(
								newSymbols[ i ].element 
							);
							changed = true;
						}
					}
					
					for ( var i = 0; i < 2; i++ ) {
						i = !!i;
						activeSymbols[ i ].blur();
						state.activeSymbols[ i ].focus( i );
					}
					
					changed && activeSign.updatePosition();
				}
			} else {
				// We've changed signs. Could be complicated.
				// TODO.
				// We could have just changed signs, added a new sign, removed a 
				// sign, pasted in a whole bunch of signs, who knows.
				
				// Check for changed signs, I'll do the rest later.
				if ( signArea.signs.indexOf( state.activeSign ) ) {
					// Don't use changeSign, could cause endless loop.
					activeSign.blur();
					state.activeSign.focus();
				}
			}
			keyboard.update();
		}
	};
	
	// TODO: Documentation
	/**
	 * @constructor
	 * @param {string} [signText] The Formal SignWriting (FSW) to be
	 *  the starting content of the sign.
	 * @cfg {""|"M"|"B"|"L"|"R"} [lane]
	 * @cfg {string} [spellingSequence]
	 * @cfg {SignSymbol[]} [symbols] Array of all SignSymbols in the Sign, including
	 *  those not yet visible/created.
	 */
	function Sign( signArea, signText ) {
		// TODO: Organize.
		this.signArea = signArea;
		this.lane = "M";
		this.spellingSequence = "";
		this.signText = signText || "";
		this.Y = 500;
		this.element = this._createDOM();
		
		if ( config.ce ) {
			this.element.contentEditable = false;
			// TO CONSIDER: 
			// Maybe just remove ce and use the ACE model of having a little 
			// text box moving around. We don't have a visible caret anymore
			// anyway, and it's not like this shares a contenteditable with the
			// rest of the box right now, so...
			// Alternatively, just move this thing outside the element. That
			// would allow easy home/end/up/down buttons. That could cause 
			// problems with backspace/delete, though. Still need to have some
			// way of catching a scroll with those, as well as the arrow keys.
			// <Realizes why placeholder text is necessary in text box>. Hmmm.
			/*
			// From ACE
			this.ce = document.createElement( "textarea" );
			this.ce.value = "\x01\x01";
			// This still doesn't fix the issues with NSTs and arrow keys, 
			// though... If those require a separate solution, maybe use 
			// whatever it might be for both.
			// Is there any way to fiddle with ranges or selections that would
			// cause a "focus" that would drag the scroll position?
			// Consider: insertNode(x);x.focus();remove(x);normalize();
			// Well, the node needs to be re-used anyway... And it might as well
			// just be an input or textarea itself... But we'll need to 
			// refocus afterward. Need some way to get the keys to work.
			//
			// Okay, a few things to consider:
			// * The most common thing requiring focus is a simple keypress or
			//   signChange on plain Signs.
			// * Arrow keys on NSTs need to be reasonably fast, or delay will be
			//   very noticable. Actually, do these even *need* focus?
			//   Probably. Any of these can change rows, and brs are in NSTs.
			*/
			this.ce = this.element.appendChild( document.createElement( "span" ) );
			this.ce.contentEditable = true;
			this.ce.className = "SWKB-Sign-ce";
		}
		//this.ce.appendChild( document.createTextNode( "\u00a0" ) );
		//this.ce.appendChild( document.createTextNode( "blah" ) );
		var thiz = this;
		addEventListener( this.element, "click", function ( e ) {
			if ( thiz !== activeSign ) {
				signArea.changeSign( thiz, true );
			}
		}, true );
		
		this.crosshairY = this.element.firstChild;
		this.crosshairX = this.crosshairY.nextSibling;
		
		/*
		// Consider removing this.
		this.FSW = this.element.appendChild(
			document.createElement( "span" )
		);
		this.FSW.className = "SWKB-FSW";
		*/
		
		// Changes to an object when on. Used for things like fingerspelling and
		// signspellingsequences.
		this.mode = false; 
		
		this.symbols = [ new SignSymbol( null, this ), new SignSymbol( null, this ) ];
		this.symbols[ 1 ].rightHand = true;
		this.pseudos = [];
		
		if ( signText ) {
			// Spelling sequences
			// I'll need to ask someone what to do with these at some point.
			// For now, stuff them into the sign as a variable that has no
			// visible effects, but still adds the wikitext.
			signText = signText.replace( 
				spellingSequenceReg, 
				function( ss ) {
					thiz.spellingSequence = ss;
					// Afterwards, strip them out.
					return "";
				}
			);
			
			this.lane = signText.charAt( 0 );
			if ( this.lane === "S" ) {
				this.lane = "";
			}
			
			// The contents should not be parsed beyond what's necessary to get
			// the correct images and positions until focused.
			this.unparsed = true;
			
			// Build each SignSymbol.
			
			fswParser.getAllSymbols( signText, function ( symbolText ) {
				thiz.symbols.splice( 
					thiz.symbols.length - 2,
					0,
					new SignSymbol( symbolText, thiz )
				);
			} );
			
			// Preserve dimensions. 
			this.bottom = this.lane !== "" ? 
				+signText.substr( 5, 3 ) :
				1000 - signText.substr( 10, 3 ); // Punctuation. 
			//this.width = +signText.substr( 5, 3 );
			//this.updatePosition();
			
			for ( var i = 0; i < this.symbols.length - 2; i++ ) {
				var symbol = this.symbols[ i ];
				// For some reason, punctuation isn't visible.
				// Fixed?
				if ( symbol.type !== symbolTypes.NONE ) {
					symbol.buildElement( true, this );
					symbol.updateImage();
				}
			}
		}
		this.updatePosition();
	}
	
	Sign.prototype = {
		/**
		 * Create the DOM for the Sign, not including contenteditable element or
		 * contained symbols.
		 * @return {HTMLElement}
		 */
		_createDOM: ( function () {
			
			var template = document.createElement( "div" );
			template.className = "SWKB-Sign";
			var crosshairY = template.appendChild(
				document.createElement( "div" )
			);
			var crosshairX = template.appendChild( 
				document.createElement( "div" )
			);
			crosshairY.className = "SWKB-crosshairY";
			crosshairX.className = "SWKB-crosshairX";
			// Consider removing this.
			var FSW = template.appendChild(
				document.createElement( "span" )
			);
			FSW.className = "SWKB-FSW";
			if ( !writingModeSupported ) {
				template.style.display = "block";
			}
			
			return function _createDOM () {
				// TO CONSIDER: Also adding event handlers here.
				var element = template.cloneNode( true ), thiz = this;
				return element;
			}
		})(),
		/**
		 * @return {string} Formal SignWriting (FSW) of the Sign, including
		 *  lane, positioning data, and all symbols.
		 */
		toString: function () {
			// Use the cached value if we're not on the activeSign.
			// This isn't really a good system, I think.
			if ( activeSign === this ) {
				var symbols = this.symbols, symbol, i = 0, w = 500, h = 500;
				for ( ; i < symbols.length; i++ ) {
					symbol = symbols[ i ];
					if ( symbol.type !== symbolTypes.NONE ) {
						w = Math.max( w, symbol.X + symbol.width );
						h = Math.max( h, symbol.Y + symbol.height );
						if ( symbol.width === 0 ) {
							// Not ideal, I think.
							return this.signText;
						}
					}
				}
				return this.signText = ( this.isEmpty() ? 
					"" : 
					( this.lane ? this.spellingSequence + this.lane + w + "x" + h : "" ) + 
						this.symbols.join("")
				);
			} else {
				return this.signText;
			}
		},
		isSign: true,
		/**
		 * Updates the boundaries of the sign and the positions of its symbols.
		 * @param {SignSymbol} specific Only update this specific symbol. If
		 *  undefined, updates all symbols.
		 * @param {boolean} stable True if no symbols have moved. (This is so
		 *  the function doesn't need to test if the upper boundary of the sign
		 *  has moved.)
		 * @return {boolean} Whether the position of the top edge has changed.
		 */
		updatePosition: function ( specific, stable ) {
			// Note: NONEs do not affect the sign's boundaries.
			
			// Should this purge the Sign's signText with toString()?
			
			var bottom = 500, top = 500, 
				// Unparsed signs use signtext to determine boundaries.
				unparsed = this.unparsed,
				size = this.signArea.size,
				style = this.element.style,
				symbols = this.symbols,
				symbol,
				symbolStyle,
				i;
			
			// Note: SignSymbol.Y is distance from the *top*.
			
			// Find the lowest and highest boundary points of the symbols.
			// (If unparsed, we don't need the lower boundary, since that can be
			// determined from the signText.)
			// (If stable, we don't need to find the upper boundary, since the 
			// cached version will still be correct.)
			for ( i = 0; i < symbols.length; i++ ) {
				// TODO: Make it relative to the size.
				
				// Ignore invisible SignSymbols for positioning purposes.
				if ( symbols[ i ].type !== symbolTypes.NONE ) {
					// If stable, use this.top instead of calculating it.
					if ( !stable ) {
						top = Math.min( top, symbols[ i ].Y );
					}
					// If unparsed, use this.bottom instead of calculating it.
					if ( !unparsed ) {
						bottom = Math.max( bottom, symbols[ i ].Y + ( symbols[ i ].height || 0 ) );
					}
				}
			}
			
			var topChanged    = !stable  && this.top    !== ( this.top    = Math.floor( top    ) ),
				bottomChanged = unparsed || this.bottom !== ( this.bottom = Math.ceil(  bottom ) ),
				// Don't shrink the Sign down to nothing, even if there's 
				// nothing in it.
				visibleTop = Math.min( this.top, 490 ),
				visibleBottom = Math.max( this.bottom, 510 );
			// PROBLEM: Punctuation doesn't have the measurements available.
			if ( !unparsed ) {
				this.bottom = Math.ceil( bottom );
			}
			
			if ( topChanged || bottomChanged ) {
				style.height = ( visibleBottom - visibleTop ) * size + "px";
			}
			
			if ( topChanged ) {
				// Position this in the center.
				this.crosshairX.style.top = ( 500 - visibleTop ) * size + "px";
			}
			
			if ( !stable ) {
				// Run through symbols, then pseudosymbols, fixing positioning
				// styles and signtext as necessary.
				for ( var fakeSymbols = 0; fakeSymbols < 2; fakeSymbols++ ) {
					symbols = fakeSymbols === 0 ? this.symbols : this.pseudos;
					for ( i = 0; i < symbols.length; i++ ) {
						symbol = symbols[ i ];
						// If the top boundary changed, update top positioning
						// of every symbol, regardless of whether only a
						// specific was targeted. 
						// Invisible symbols (.NONEs) don't need updates.
						if ( ( !specific || symbol === specific || topChanged ) && symbol.type !== symbolTypes.NONE ) {
							symbolStyle = symbol.element.style;
							
							symbolStyle.top = ( ( symbol.Y - visibleTop ) * size ) + "px";
							
							if ( !specific || specific === symbol ) {
								symbolStyle.left = ( 
									( symbol.X - 
										( 500 - config.signWidth / 2 ) + 
										( { "L" : -75, "R" : 75 }[ this.lane ] || 0 )
									) * size
								) + "px";
								
								// Slight unnecessary action here during some
								// functions like addSwitchDeleteSymbol: the
								// updateSignText isn't needed there, but it is
								// needed for other functions like centerSign,
								// and I don't feel like adding another arg for
								// just a slight performance increase.
								if ( !unparsed && !fakeSymbols ) {
									symbol.updateSignText();
								}
							}
						}
					}
				}
			}
		},
		/**
		 * Checks if the Sign has no visible symbols.
		 * @return {boolean}
		 */
		isEmpty: function () {
			var symbols = this.symbols;
			return symbols.length === 2 &&
				symbols[ 0 ].type === symbolTypes.NONE &&
				symbols[ 1 ].type === symbolTypes.NONE;
		},
		/**
		 * Removes focus from the sign.
		 */
		blur: function () {
			// TODO: Do general maintainance when blurring. Consider autocentering,
			// and maybe dump the symbolTypes.NONE symbols if they're still there.
			
			// Make it cache signText. This is not a good way to do things.
			this.toString();
			
			for ( var i = 0, symbol; i < 2; i++ ) {
				symbol = activeSymbols[ !!i ];
				// There should always be a activeSymbols here, but some buggy
				// browsers still trip here on occasion.
				symbol && symbol.blur();
				
				activeSymbols[ !!i ] = undefined;
			}
			this.element.className = "SWKB-Sign";
			
			if ( this.blinkTimer ) {
				if ( this.blinkStatus ) {
					this.blink();
				}
				clearTimeout( this.blinkTimer );
				delete this.blinkTimer;
			}
			
			// REDUNDANT w/ NonSignText. TODO: Fix.
			if ( this.isEmpty() ) {
				// removeSign( this );
			}
		},
		/**
		 * Focus the sign.
		 */
		focus: function () {
			// Why doesn't this set activeSign?
			// TODO: Figure that out.
			
			activeSign = this;
			
			if ( this.unparsed ) {
				this.parseContents();
			}
			
			this.element.className = "SWKB-Sign SWKB-activeSign";
			
			this.focusSymbols();
			
			// Make the crosshairs blink like a cursor.
			// Only runs in non-CSSAnimation-supporting browsers.
			if ( !animationsSupported && !this.blinkTimer ) {
				var thiz = this;
				( function timedBlink () {
					thiz.blink();
					thiz.blinkTimer = setTimeout( timedBlink, 530 );
				} ) ();
			}
			
			if ( config.ce ) {
				
				keyboard.show();
				
				// TODO: Merge these two sections somehow. Don't have redundant
				// caret moves. The proper order should be this:
				// 1. Remove caret from old sign.
				// 2. Add caret to text box.
				// 3. Remove caret from text box.
				// 4. Add caret to Sign element.
				// Instead, there are extra moves before step 4, where the caret
				// is added to the old sign, and then removed again.
				// Also, the logic here should be moved to caret.
				// console.log( "Sign focus:", this, this.parentNode );
				caret.busy = true;
				caret.selectElement( this.ce, false, true );
				caret.keepInView();
				/*
				caret.textBoxSync( true );
				
				*/
				caret.busy = false;
				// console.log( "Sign focus complete" );
				// this.element.scrollIntoView(); // or something like that.
				// scrollIntoView itself won't work very nicely. It always aligns
				// the top/side of the viewport/box.
			}
			/*
			this.element.tabIndex = "0";
			this.element.focus();
			this.element.blur();
			this.element.tabIndex = undefined;
			*/
		},
		focusSymbols: function () {
			var symbols = this.symbols, firstHand;
			symbols[ 0 ].focus();
			firstHand = symbols[ 0 ].rightHand;
			for ( var i = 1, symbol; ( symbol = symbols[ i ] ); i++ ) {
				if ( symbol.rightHand !== firstHand ) {
					symbol.focus();
					break;
				}
			}
		},
		blink: function () {
			this.blinkStatus = !this.blinkStatus;
			this.crosshairX.style.backgroundColor = 
				this.crosshairY.style.backgroundColor =
				this.blinkStatus ? "#AAA" : "";
		},
		setDomSize: function () {
			var style = this.element.style, size = this.signArea.size;
			style.width = config.signWidth * size + "px";
		},
		/**
		 * Reposition the various symbols in the sign such that the center of 
		 * the bounding box of all of the body and head symbols (or all of the 
		 * visible symbols if there are no head or body symbols) are at 500x500,
		 * the center of the sign.
		 */
		centerSign: function () {
			// TODO: Make this a bit simpler.
			function isStable( symbol ) {
				return symbol.type === symbolTypes.HEAD || 
					( symbol.type === symbolTypes.BODY && symbol.symbolText < "S376" );
			}
			var symbols = this.symbols, symbol, minX, minY, maxX, maxY, i = 0, 
				YDiff, XDiff,
				hasStable = false;
			for ( ; i < symbols.length; i++ ) {
				symbol = symbols[ i ];
				if ( symbol.width === 0 && symbol.type !== symbolTypes.NONE ) {
					// The image hasn't finished loading. Not able to center yet.
					return false;
				}
				if ( hasStable === false && isStable( symbol ) ) {
					hasStable = true;
					minX = minY = maxX = maxY = undefined;
				}
				if ( symbol.type !== symbolTypes.NONE && hasStable === false || isStable( symbol ) ) {
					minX = minX ? Math.min( minX, symbol.X ) : symbol.X;
					minY = minY ? Math.min( minY, symbol.Y ) : symbol.Y;
					maxX = maxX ? Math.max( maxX, symbol.X + symbol.width ) : symbol.X + symbol.width;
					maxY = maxY ? Math.max( maxY, symbol.Y + symbol.height ) : symbol.Y + symbol.height;
				}
			}
			XDiff = 500 - Math.floor( ( maxX + minX ) / 2 );
			YDiff = 500 - Math.floor( ( maxY + minY ) / 2 );
			if ( XDiff || YDiff ) {
				for( i = 0; i < symbols.length; i++ ) {
					symbol = symbols[ i ];
					symbol.X += XDiff;
					symbol.Y += YDiff;
				}
			}
			// Update the position and resulting signText of all symbols.
			this.updatePosition();
		},
		
		/**
		 * Set content attributes (like .face, .fingers, .plane, alt properties,
		 * etc.) of all SignSymbols contained by this Sign, based on their
		 * preexisting symbolText/FSW.
		 */
		parseContents: function () {
			// I want to avoid using updateImage this entire time. That's pretty
			// important, because that's some heavy stuff.
			for ( var i = this.symbols.length; i--; ) {
				if ( this.symbols[ i ].type !== symbolTypes.NONE ) {
					this.symbols[ i ].useSymbolFSW();
				}
			}
			
			this.unparsed = false;
		},
		/**
		 * @return {Number}
		 */
		textOffset: function () {
			// TODO: Document
			var signs = signArea.signs,
				index = this.index(),
				signText = this.toString();
			if ( index === 0 ) {
				return 0;
			} else {
				var pre = fswParser.processMixedText( 
						signs.slice( 0, index ).join( " " ) + " " + signText
					).length - signText.length;
				//console.log( "TEXTOFFSET: ", pre );
				return pre;
				/*
					fswParser.processMixedText( 
						signArea.signs.slce( 0, signIndex ).join(" ")
					).length + ( signIndex > 0 ),
				*/
			}
		},
		/**
		 * @return {Number} Index of the position of the Sign in its SignArea.
		 */
		index: function () {
			return this.signArea.signs.indexOf( this );
		},
		/**
		 * @param {Number} n
		 * @return {SignSymbol} The nth visible (type!=none) symbol in the sign.
		 */
		getVisibleSymbol: function ( n ) {
			for ( var symbols = this.symbols, l = symbols.length, i = 0, ii = 0; i < l; i++ ) {
				if ( symbols[ i ].type !== symbolTypes.NONE ) {
					if ( n === ii++ ) {
						return symbols[ i ];
					}
				}
			}
			return undefined;
		}
	};
	
	/**
	 * @constructor
	 * @param {string} [signText] The FSW of the symbol.
	 * @param {Sign} sign
	 * @cfg {number[]} [alt] Array of values for the layout to store data.
	 * @cfg {number} [X] Horizontal position of the SignSymbol. Center is 500.
	 * @cfg {number} [Y] Vertical position of the SignSymbol. Center is 500.
	 * @cfg {boolean} [rightHand] For split keyboards only: True if controlled
	 *  by the righthand side, false otherwise. If keyboard isn't split, always
	 *  set to false.
	 * @cfg {number} [type] Should be symbolTypes.NONE if symbol has yet to be
	 *  visible/created, symbolTypes.UNSPECIFIED if not recognizable by the
	 *  layout, and anything else to be made use of by the layout where 
	 *  applicable.
	 * @cfg {Sign} [sign]
	 * @cfg {string} [signText] Current FSW of the SignSymbol, empty string if not
	 *  yet created.
	 */
	function SignSymbol( signText, sign ) {
		this.rightHand = false; //rightHand;
		this.sign = sign || activeSign; // Add some way to override, I think.
		// Make sure this doesn't cause issues with cloning symbols.
		
		// TODO: The elements don't need creating for fakes, do they?
		var thiz = this, 
			element = this.element = this._createDOM( config.useFont );
		
		if ( !config.useFont ) {
			element.onload = function () {
				thiz.updateDimensions();
			};
			if ( this.sign && transformSupported ) {
				//var size = this.sign.signArea && this.sign.signArea.size;
				var size = signArea && signArea.size;
				if ( size && size !== 1 ) {
					this._transform( "scale(" + size + ")" );
				}
			}
		}
		
		// Various properties...
		this.rotation = 0;
		
		// Not sure if we should always assume that this is a left-unparsed
		// symbol whenever signText is given.
		this.type = signText ? symbolTypes.UNSPECIFIED : symbolTypes.NONE;
		
		// Positioning. 500 is the center, 250 and 750 are the normal edges.
		this.X = 492;
		this.Y = 485;
		// Planes. 0 = Wall plane, 1 = Floor plane.
		this.plane = 0;
		this.face = 2;
		this.fingers = 0; // Active fingers. Small=1, Ring=2, ..., Thumb=16
		this.alt = [ 0, 0, 0, 0, 0, 0, 0, 0, 0 ];
		this.bothHands = 0; // For use in movement arrows and faces
		
		
		// Should always either be "" or match /^S[1-9a-f]{5}$/
		this.symbolText = ""; // TO CONSIDER: Removing the "S".
		
		// signText also includes position data. This should always either be ""
		// or match /^S[1-9a-f]{5}\d{3}x\d{3}$/
		this.signText = signText || "";
		
		// Dimensions of the symbol's image.
		// Nothing is to modify these except for this.updateDimensions(). 
		// TODO: Consider actually enforcing that via keeping them private.
		this.height = 0;
		this.width = 0;
		
		// TODO: Put this somewhere else. This doesn't need to run until the 
		// DOM is added.
		this.addListeners();
		
		// Interpret signText if given.
		if ( signText ) {
			this.parseFSW( signText );
			//this.useSymbolFSW();
		}
		
		// Does calling this ever do anything? updatePosition doesn't work 
		// unless the activeSymbol's symbols include the symbol.
		//this.updatePosition();
	}
	
	SignSymbol.prototype = {
		/**
		 * Create the DOM for the SignSymbol. (Not to be confused with the 
		 * badly-named buildElement(), which adds the DOM to the Sign when the
		 * SignSymbol should be visible to the user.)
		 * @return {HTMLElement}
		 */
		_createDOM: ( function () {
			var fontTemplate = document.createElement( "span" ), 
				imgTemplate = document.createElement( "img" );
			
			fontTemplate.className = "SWKB-Symbol";
			
			return function _createDOM( useFont ) {
				var element = 
					( useFont ? fontTemplate : imgTemplate ).cloneNode( true );
				return element;
			};
		} )(),
		_transform: ( function () {
			// Maybe instead of all this, the image could have plain height and
			// width applied to it, using .naturalHeight/Width?
			
			var transform, i = 0, options = [ 
				"transform", 
				"-webkit-transform", 
				"-moz-transform",
				"-ms-transform",
				"-o-transform"
			];
			for ( ; i < options.length; i++ ) {
				if ( options[ i ] in signListElem.style ) {
					transform = options[ i ];
					break;
				}
			}
			return function _transform( val ) {
				this.element.style[ transform ] = val;
			};
		} )(),
		/**
		 * Add event listeners to SignSymbol.element.
		 */
		addListeners: function () {
			var thiz = this, element = this.element;
			// Select the SignSymbol when clicked on.
			addEventListener( element, "click", function () {
				// Alternative method of switching active symbols:
				// Clicking on the element makes it active.
				if ( thiz.sign === activeSign ) {
					signArea && signArea.history.pushState();
					activeSymbols[ thiz.rightHand ].blur();
					thiz.focus();
					keyboard.update( thiz.rightHand );
				}
			}, true );
			
			// Mouse dragging. TODO: Touch events, attachEvent support.
			// I'm not sure about the performance impact of this. It might be too
			// high for this to be worth it. TODO: Tests and such.
			var dragging = false;
			// element.ontouchstart = 
			element.onmousedown = function ( e ) {
				if ( dragging ) {
					return;
				}
				
				if ( thiz.sign === activeSign && thiz.sign.signArea === signArea ) {
					// Problem with this condition is that then we just can't 
					// undo... Better than breaking, I guess.
					signArea.history.pushState();
				}
				
				e = e || window.event;
				
				var x = e.clientX, y = e.clientY;
				// TODO: Choose a different name. This one's already taken.
				// TODO: Work with size.
				function move( e ) {
					e = e || window.event;
					var xDelta = x - ( x = e.clientX ), 
						yDelta = y - ( y = e.clientY );
					if ( yDelta || xDelta ) {
						thiz.X -= xDelta;
						thiz.Y -= yDelta;
						thiz.fitToConstraints();
						thiz.updatePosition();
					}
					return false;
				}
				function end() {
					removeEventListener( document.body, "mousemove", move, false );
					removeEventListener( document.body, "mouseup", end, false );
					
					dragging = false;
				}
				
				addEventListener( document.body, "mousemove", move, false );
				addEventListener( document.body, "mouseup", end, false );
				
				return false;
			};
		},
		/**
		 * Update the position of the symbol's element to match its X and Y
		 * properties.
		 */
		updatePosition: function () {
			var sign = this.sign;
			if ( sign ) {
				sign.updatePosition( this );
			}
		},
		
		// TODO: Split this function. Some parts are applicable regardless of
		// useFont, some are not.
		setDimensions: function () {
			if ( fontHandler.loaded /* config.useFont */ ) {
				var s = fontHandler.size( this.symbolText );
				if ( s ) {
					// NOTE: Chrome height are a bit off. line-height: 30px; 
					// seems to fix it, but I doubt that works in variable
					// sizes.
					// Hm, just noticed that 30px is the default font size.
					// Presumably, line-height can be just set to font size.
					s = s.split( "x" );
					widthCache[  this.symbolText ] = +s[ 0 ];
					return heightCache[ this.symbolText ] = +s[ 1 ];
				}
			} else {
				widthCache[ this.symbolText ] = this.element.offsetWidth;
				heightCache[ this.symbolText ] = this.element.offsetHeight;
			}
		},
		/**
		 * 
		 */
		updateDimensions: function () {
			// Okay, some rewriting needed. Even if using images, if the caches
			// already have dimension data, resolve stuff before the load event
			// fires, and then *don't* run it again. Somehow. Not sure how.
			// Maybe a dedicated property for "awaiting load"?
			// All three vars here are currently unused.
			var width, height, changed = false;
			
			if ( !widthCache[ this.symbolText ] ) {
				this.setDimensions();
			}
			
			width  = this.width  = widthCache[ this.symbolText ] || 0;
			height = this.height = heightCache[ this.symbolText ] || 0;
			
			/*
			if ( config.useFont && width ) {
				this.element.style.width = width + "px";
				this.element.style.height = height + "px";
			}
			*/
			
			/*
			if ( this.X + width > 500 + config.signWidth / 2 ) {
				this.X = 500 + config.signWidth / 2 - width;
				// changed = true;
			}
			
			if ( this.Y + height > 500 + config.signHeight / 2 ) {
				this.Y = 500 + config.signHeight / 2 - height;
				// changed = true;
			}
			*/
			this.fitToConstraints();
			
			// TODO: Fix this so that we don't have to check it every single time.
			// Currently, this is run on every keystroke. Onload calls this.
			
			if ( !this.sign.unparsed ) {
				this.sign.updatePosition( this, true );
			}
			
		},
		fitToConstraints: ( function () {
			var minX = 500 - config.signWidth  / 2, maxX = 1000 - minX,
				minY = 500 - config.signHeight / 2, maxY = 1000 - minY;
			return function () {
				this.X = Math.max( minX, Math.min( maxX - ( this.width || 0 ),  this.X ) );
				this.Y = Math.max( minY, Math.min( maxY - ( this.height || 0 ), this.Y ) );
			}
		})(),
		// This needs to be split. signText needs to be updated even if we only
		// changed some positioning, but we don't need any of this other stuff,
		// ie updating src, running symbolFSW, etc. Backend stuff should be 
		// split off, I think. this.signText = this.symbolText ? sT + X + Y...
		// Needs more consideration. How frequently should this be run?
		// Should updateImage itself call the signText update?
		
		// When creating a symbol, no need to updateSignText, but position and
		//  image need updating. (position uses updateAll arg here.)
		// When changing a symbol, signText and image need update.
		// When changing mid-keystroke (rotate), signText and image.
		// When repositioning, either by holding a key or by mouse, 
		//  updatePosition and updateSignText, but updateImage is not needed.
		// When parseContents, no need to updateSignText. Update image and pos.
		//   -Scratch that, we don't need any of that. signText is set on 
		//    creation, and so is position, both by interpretFSW.
		// On updatePosition called by other symbols, don't updateSignText, 
		//  except when we're also doing a centerSign, in which case uST.
		//  (Unclear. When do other signs call uP?)
		// When preparing a fake, updateImage and signText.
		// 
		// So that's:
		// *      uP(0,1), uI
		// * uST,          uI
		// * uST,          uI // from layout
		// * uST, uP          // from layout
		// *      uP
		// * uST, uP
		// ...
		// TODO: Finish listing all scenarios.
		// 
		// So, it's already clear that the default uI and uP behaviours need to
		// include a uST somehow. However, large pages are going to have 
		// hundreds of symbols, each of which would call uST twice on creation
		// if all the uIs and uSTs called it, and neither are necessary there.
		// 
		/**
		 * Update FSW and visible DOM of the symbol.
		 */
		updateImage: function () {
			var image;
			
			if ( this.type === symbolTypes.UNSPECIFIED ) {
				// Maybe this should actually just use symbolText directly?
				image = this.symbolText;
			} else {
				image = layout.symbolFSW.call( this );
				image = this.symbolText = image ? "S" + image : "";
			}
			
			// Maybe move this into the part that only runs for non-fakes.
			//this.signText = image ? this.symbolText + this.X + "x" + this.Y : "";
			this.updateSignText();
			
			if ( !this.fake && this.type !== symbolTypes.NONE ) {
				// New plan: Mine image data out of the canvas, and set src.
				// Remember, needs both fonts. 
				// Work in fontHandler. Maybe add a function there to get the
				// base64 data.
				if ( fontHandler.loaded ) {
					if ( config.useFont ) {
						var key = image;
						// Use sw10.code, imported straight from Stephen Slevinski's
						// sw10.js. 
						var code = fontHandler.code( key );
						this.element.setAttribute( "data-SWKB-symbol", code );
						
						// Sizing. If the size is cached, use that. Otherwise,
						// request size, and cache it. If the font hasn't loaded
						// yet... Maybe add it to an array of "waiting"s and run
						// size on them as soon as possible?
						this.updateDimensions();
					} else {
						this.updateDimensions();
						var color = this.color || 
							( this.isActive &&
								( this.rightHand ? "00CC00" : "0000FF" )
							);
						this.element.src = image ? fontHandler.getImage( image, color ) /* + "000x000" */ : "";
					}
				} else {
					var src = image ? symbolServer + /* "M100x100S" + */ image /* + "000x000" */ : "";
					
					if ( ( this.color || this.isActive /* activeSymbols[ this.rightHand ] === this */ ) && src ) {
						src += "&line=" + ( this.color || ( this.rightHand ? "00CC00" : "0000FF" ) );
					}
					
					/*
					if ( this.color && src ) {
						src += "&line=" + this.color;
					}
					*/
					this.element.src = src;
				}
			}
		},
		
		updateSignText: function () {
			// I'm having difficulty figuring out how expensive this is...
			return this.signText = this.symbolText ? 
				this.symbolText + this.X + "x" + this.Y : 
				"";
		},
		/**
		 * Add the SignSymbol's element to the visible DOM.
		 */
		buildElement: function ( isNotActive, sign ) {
			// this.sign = sign || activeSign;
			
			if ( this.fake ) {
				return;
			}
			
			this.element = ( sign || this.sign || activeSign ).element
				.appendChild( this.element );
			//this.updateImage();
			// Why does this call updatePosition? Is this necessary? It takes up
			// a lot of resources.
			// TODO: Figure this out. For now, turn off. When !unparsed.
			if ( !this.sign.unparsed ) {
				this.updatePosition();
			}
			
			// Is this necessary?
			if ( !isNotActive ) {
				//this.element.className = "activeSymbol";
				this.focus();
			}
			return this.element;
		},
		/**
		 * Remove the SignSymbol from the Sign.
		 */
		remove: function () {
			// This can't be used with PseudoSymbol, since this directly 
			// references .symbols...
			// Should this be a Sign method instead?
			var symbols = this.sign.symbols;
			symbols.splice( symbols.indexOf( this ), 1 );
			this.removeElement();
		},
		/**
		 * Remove the SignSymbol's element from the DOM.
		 */
		removeElement: function () {
			this.sign.element.removeChild( this.element );
		},
		/**
		 * @return {string} Formal SignWriting (FSW) for the symbol, including
		 *  position data and the "S" at the beginning.
		 */
		toString: function () {
			// This really had better not be necessary. toString is getting 
			// called all the time, even if it's limited to when activeSign is
			// the symbol's sign.
			// this.updateImage();
			
			return this.signText;
		},
		/**
		 * Focuses the symbol. 
		 */
		focus: function ( rightHand ) {
			// TODO: Make this work for layouts that don't use two hands.
			activeSymbols[ rightHand !== undefined ? rightHand : this.rightHand ] = this;
			this.isActive = true;
			config.useFont || this.updateImage();
			if ( this.type !== symbolTypes.NONE ) {
				// this.element.className = "activeSymbol";
				this.element.className = 
					this.rightHand ? "activeSymbol-R" : "activeSymbol-L";
			}
		},
		/**
		 * Removes focus from the symbol.
		 */
		blur: function () {
			this.isActive = false;
			this.element.className = "";
			config.useFont || !config.noFont || this.updateImage();
		},
		/**
		 * Set position and signText properties, based on FSW given.
		 * @param {string} signText Formal SignWriting of the symbol.
		 */
		parseFSW: function ( signText ) {
			// Needs some changes here.
			// For one thing, symbolFromFSW is pretty heavy. Might make sense to
			// have symbols as UNSPECIFIED until focused. Actually, no, need a 
			// separate type to distinguish from actually typeless symbols.
			// (Specific: symbolFromFSW takes up a lot of time. Too heavy.)
			// Maybe "TBD", or something. 
			// So, modify SignSymbol.focus, I think.
			// Point two: PseudoSymbol needs to borrow the first part.
			// Probably should just be a direct ref from the prototype.
			// Point three: Layout change needs to borrow the second part.
			// Split, maybe? Need some names, though.
			// Side note: UNSPECIFIED currently stores symbolText as alt0.
			// Should probably just not set anything other than symbolText
			// and signText, and store them there. Same for TBD.
			// Problem: rightHand needs to be set in advance for Next SignSymbol 
			// to work properly. 
			// Maybe just parse when the sign is activated?
			// Instead of setting a property on the SignSymbol, set a property
			// on the Sign, maybe?
			//var symbolReg = /(S[123][0-9a-f]{2}[0-5][0-9a-f])([0-9]{3})x([0-9]{3})/g;
			// Is a regex need at all here?
			
			// Note: This function runs thousands of times, once for every 
			// symbol on the page, even if it's never focused. Performance is 
			// important.
			
			var val = symbolReg.exec( signText || this.signText ),
				symbolCode = val[ 1 ], // redundant
				result;
			
			this.X = + val[ 2 ]; // Um, would it be faster to use substr?
			this.Y = + val[ 3 ];
			this.signText = signText;
			this.symbolText = val[ 1 ];
		},
		/**
		 * Set content properties (rightHand, alt, face, plane, etc.) of the 
		 *  symbol, based on the available FSW in the signText property.
		 */
		useSymbolFSW: function () {
			// symbolFromFSW does the work here. Is it bad practice to put it
			// straight into a condition?
			
			// var symbolCode = this.signText.substr( 1, 5 ); // Unnecessary.
			var symbolCode = this.symbolText.substr( 1 ); 
			// Seriously need to decide whether symbolCode should include the
			// letter "S" at the beginning or not...
			
			
			if ( layout.symbolFromFSW( symbolCode, this ) === false ) {
				// symbolFromFSW failed, could not find the symbol anywhere.
				this.type = symbolTypes.UNSPECIFIED;
				// Just so it can be accessed from either side...
				this.bothHands = 1;
				this.rightHand = false;
				// Um, is this necessary? Should've been already run elsewhere,
				// right? TODO: Check.
				this.updateImage();
			} else {
				// Not sure I need to do anything here. symbolFromFSW took care
				// of all the props that need setting.
				//this = result;
			}
		},
		/**
		 * Copies over all properties from one symbol to another
		 * @param {SignSymbol} [target] SignSymbol onto which 
		 *  properties should be copied. If omitted, new SignSymbol is created.
		 * @return {SignSymbol} 
		 */
		clone: function ( target ) {
			// TODO fakeSymbol
			target = target || new SignSymbol();
			
			for ( var p in this ) { // Clone
				// maybe the alt check should be replaced with a typeof "object" check
				if ( this.hasOwnProperty( p ) && p !== "alt" && p !== "element" ) { 
					target[ p ] = this[ p ];
				}
			}
			for ( p = 0; p < this.alt.length; p++ ) {
				target.alt[ p ] = this.alt[ p ];
			}
			return target;
		}
	};
	
	/**
	 * A fake symbol, for purposes of autocomplete and tutorial.
	 * @constructor
	 */
	function PseudoSymbol( signText, color ) {
		this.X;
		this.Y;
		this.symbolText;
		
		this.type = symbolTypes.UNSPECIFIED;
		this.color = color || "77DDFF";
		
		if ( config.useFont ) {
			this.element = document.createElement( "span" );
			// Issue: SWKB-PS changes the z-index so that it's over the regular
			// symbols, blocking them out and making it difficult to type.
			// TODO: Fix.
			this.element.className = "SWKB-Symbol SWKB-PS";
			this.element.style.color = "#" + this.color;
		} else {
			this.element = document.createElement( "img" );
			this.element.className = "SWKB-PS";
		}
		
		// I'm probably going to need to change this line. Maybe add a param.
		this.sign = activeSign;
		
		this.symbolText = signText.substr( 1, 5 );
		
		this.parseFSW( signText );
		this.buildElement( true, activeSign );
		this.updateImage();
		this.updatePosition();
		
		// We need to be able to determine:
		// * The FSW
		// * The positioning
		// All other fancy attributes are irrelevant until chosen, and at that
		// point we're not dealing with PS directly anyway.
		// To do this, we'll need to use symbolReg from SignSymbol.parseFSW,
		// as well as the first several lines of code there.
		// Not so relevant, but I really need to cache all of the regexps.
		// Maybe a single object? Hmm...
	}
	
	PseudoSymbol.prototype = SignSymbol.prototype;
	
	/*
	// NonSignText stuff
	//
	// So, here's the plan for inputting non-sign text:
	// * It should be possible to easily switch back and forth from editing
	//   signs to editing plain text, using the same method. 
	//   Options: Mouse, nonshift ` key, some combo of Ctrl/Shift/Alt/Something,
	//   or perhaps integrate into ULS, allowing Ctrl-M use. Leaning towards `.
	// * Normal arrow keys need to work. Up goes up, etc. Browsers currently
	//   can't handle vertical-lr and contenteditable combinations.
	// * Ideally, normal text-editing stuff should work. This includes:
	// ** Highlighting, either with mouse or keyboard.
	// *** Even across signs.
	// ** Copying, pasting.
	// *** Sounds complicated. Copying maybe should just listen on things.
	// ** Line breaks, except on input tags, in which case enter should submit.
	// * Normal wikitext-fiddling buttons, maybe?
	// ** Yes. This will require updating of the caret on the text field.
	// 
	// For the time being, there is (or at least should be) no way for these to
	// appear except when config.ce is set to true and the ` extrakey is set.
	//
	// Known bugs:
	// * A lone empty NST will not focus. New text inputted will not be inside
	//   the appropriate element.
	// ** Solution: Watch the focusNode, and if it's an "orphan", add it to
	//    activeSign.
	// * Line breaks still don't handle well.
	// ** Possible solution: Have NST.empty, an invisible text node, which gets
	//    removed as soon as it's no longer necessary, or when the NST isn't 
	//    active.
	// * Using rare keys, text can be typed into Sign contenteditables.
	// ** Option 1: Watch for these, and have added text trigger a 
	// ** togglePlainText with the text being added there.
	// ** Option 2: Ignore it. Font-size 0 makes it invisible anyways.
	// * Various situations can lead to two adjacent NonSignTexts. Examples:
	// ** Using togglePlaintext on Sign preceded by NST or followed by NST.
	// *** Or both.
	// ** Deleting a Sign surrounded by NSTs.
	// *** Even indirectly, IE by leaving an empty Sign.
	// ** Adding HTML (bolding, etc) to the NST breaks things. TODO: Suppress
	//    all added HTML.
	//
	// Okay, new idea for dealing with messy stuff (ie caret after newline before
	// Sign, dealing with line breaks, etc):
	//  Set up an inline-block element that continually dumps contents :P
	//  Would that actually work? Can you focus empty inline-blocks?
	*/
	/**
	 * @constructor
	 * @param {string} text The starting text content of the NonSignText.
	 */
	function NonSignText( signArea, text ) {
		var thiz = this;
		var element = this.element = document.createElement( "span" );
		this.signText = text || "";
		this.signArea = signArea;
		element.onfocus = function () {
			// ... I'm not sure this can ever actually happen.
			if ( thiz !== activeSign ) {
				activeSign.blur();
				activeSign = thiz;
				// activeSign.focus( /*false*/ );
			}
		};
		element.className = "SWKB-NonSign";
		
		if ( caret.ieSelection ) {
			// This was supposed to prevent old IEs from going crazy. Did it
			// actually work?
			element.hideFocus = true;
			element.mousedown = element.onmouseup = function () {
				var r = document.body.createTextArea();
				r.moveToElementText( element );
				r.select();
				return false;
			};
		}
		
		// element.contentEditable = true;
		if ( text ) {
			element[ innerText ] = text;
		}
		
		this.symbols = []; // Is this necessary?
	}
	
	NonSignText.prototype = {
		/**
		 * @return {string} returns the text content of the NonSignText's element.
		 */
		toString: function () {
			return this === activeSign ? this.useInnerText() : this.signText;
		},
		isSign: false,
		useInnerText: ( function () {
			// Fake innerText. innerText is buggy in IE, so use this instead.
			// TODO: Find some way to check if regular innerText works, and use
			// that instead if possible.
			function getInnerText( elem ) {
				var children = elem.childNodes, chLen = children.length, 
					text = "", i = 0, child;
				for ( ; i < chLen; i++ ) {
					child = children[ i ];
					text += child.nodeType === 3 ?
						// Replace non-breaking spaces with regular ones.
						// Unfortunately, this will mess up actual nbsps.
						child.nodeValue.replace( /\xA0/g, " " ) :
						// Strictly speaking, the only non-br elements should be
						// the caretHolder. Maybe check against that instead of
						// checking the nodeName.
						( 
							child.nodeName === "BR" ? 
							"\n" : 
							getInnerText( child )
						);
				}
				return text;
			}
			
			// TODO: Return a simple function for supporting browsers.
			// In Chrome, fake is roughly 10% slower by my most recent test.
			return function useInnerText() {
				// TODO: Use innerText in browsers other than IE.
				var text = getInnerText( this.element );
				/*
				if ( text !== this.element[ innerText ] ) {
					console.log( "MISMATCH", text, this.element[ innerText ] );
				}
				*/
				return this.signText = text;
			};
		} )(),
		
		/**
		 * @return {boolean} Whether the NonSignText contains any content.
		 */
		isEmpty: function () {
			return this.toString() === "";
		},
		
		blur: function () {
			// ...
			try {
				this.element.blur();
			} catch ( e ) {
				signArea && signArea.updateFromDOM();
			}
			//this.normalize();
			// REDUNDANT w/ Sign. TODO: Fix.
			if ( this.isEmpty() ) {
				// removeSign( this );
			}
			this.useInnerText();
		},
		
		focus: function ( suppressCaret ) {
			// this.element.focus();
			// This shouldn't do anything if  the element is already focused.
			// TODO: Fix.
			//console.log( "Focusing NonSignText: ", this );
			
			activeSign = this;
			
			keyboard.hide();
			
			if ( suppressCaret !== true ) { // Hm, bad idea?
				signArea.element.focus(); // TODO: Check if this is necessary.
				// Might have been for FF.
				caret.selectElement( this.element );
			}
		},
		
		normalize: ( function () {
			var div = document.createElement( "div" ), normalNormalize;
			div.appendChild( document.createTextNode( "" ) );
			div.normalize && div.normalize();
			normalNormalize = !div.firstChild;
			if ( !normalNormalize ) {
				return function normalize() {
					var elem = this.element, 
						ch = elem.childNodes, 
						currentNode,
						i = 0,
						l = ch.length,
						wasText;
					for ( ; i < l; i++ ) {
						currentNode = ch[ i ];
						if ( currentNode.nodeType === 3 ) {
							if ( currentNode.nodeValue === "" ) {
								elem.removeChild( currentNode );
								i--;l--;
							} else if ( wasText ) {
								// TODO (important): Either find a way to merge
								// these cleanly, or save and reset the caret.
								//ch[ i - 1 ].appendData( currentNode.nodeValue );
								//ch[ i - 1 ].data += currentNode.data;
								ch[ i - 1 ].nodeValue += currentNode.nodeValue;
								elem.removeChild( currentNode );
								i--;l--;
							} else {
								wasText = true;
							}
						} else {
							wasText = false;
						}
					}
				};
			} else {
				return function normalize() {
					// Huh, turns out there's a native function for this.
					this.element.normalize();
				};
			}
		} )(),
		// Unused
		clean: function () {
			if ( this.extraBr && this.extraBr.nextSibling ) {
				// this.element.removeChild( this.extraBr );
				delete this.extraBr;
			}
		},
		
		maintain: function () {
			// So, the issue with line breaks is that browsers behave funny when
			// there's a line break with nothing after it. So, we make sure that
			// there's always something after the last line break. A simple 
			// inline-block div that will hopefully not be noticable, called
			// "caretHolder". Keep in mind that leaving this div with stuff in
			// it will seriously mess up certain things. Also, this function
			// will move the caret on occasion.
			// TODO: Consider adding an arg that replaces the caret.
			
			this.normalize(); // temporary
			
			var cH = this.caretHolder;
			
			if ( caret.midDeletion ) {
				caret.midDeletion = false;
				console.log( "MIDDELETION", cH && cH.childNodes );
				if ( cH && cH.firstChild && cH.firstChild.nodeName === "BR" ) {
					console.log( "REMOVING BR" );
					cH.removeChild( cH.firstChild );
				}
				// Deletion can, on occasion destroy the element and leave a br
				// behind in its place.
				var index = this.index();
				if ( !this.element.parentNode && index !== -1 ) {
					// Panic, pretty much.
					// Option 1: Kill the NST, and try to wipe the br.
					// Wait, can't kill it if there's no other signs. Hm.
					/*
					signArea.signs.splice( index, 1 );
					if ( signArea.element.childNodes[ index ].nodeName === "BR" ) {
						signArea.element.removeChild( signArea.element.childNodes[ index ] );
					}
					( signArea.signs[ index ] || signArea.signs[ index - 1 ] ).focus();
					this.focus();
					*/
					// Option 2: Attempt to re-add the NST, and wipe the br.
					// Also empty the NST. This might become an issue after
					// the selection stuff is working.
					
					var nextSign = signArea.signs[ index + 1 ];
					if ( signArea.element.childNodes[ index ].nodeName === "BR" ) {
						signArea.element.removeChild( signArea.element.childNodes[ index ] );
					}
					for ( ; this.element.firstChild; ) {
						this.element.removeChild( this.element.firstChild );
					}
					if ( nextSign ) {
						signArea.element.insertBefore( this.element, nextSign.element );
					} else {
						signArea.element.appendChild( this.element );
					}
					this.focus();
					
					return;
				}
			}
			
			// Okay, normalize doesn't even work. Try this:
			if ( this.element.lastChild ) {
				// Manually remove empty text nodes.
				for ( ; this.element.lastChild && 
					this.element.lastChild.nodeType === 3 &&
					this.element.lastChild.nodeValue === "";
				) {
					console.log( "REMOVING", this.element.lastChild );
					this.element.removeChild( this.element.lastChild );
				}
			}
			
			// console.log( "CONTENTS", this.element.childNodes );
			
			if ( cH ) {
				if ( cH.parentNode === this.element ) {
					// If we have a caretHolder, but it's no longer necessary, 
					// remove it. Even if it is still necessary, dump the contents.
					for ( ; cH.firstChild; ) {
						this.element.insertBefore( cH.firstChild, cH );
					}
					if ( cH.nextSibling || 
						( cH.previousSibling && cH.previousSibling.nodeName !== "BR" )
					) {
						this.caretHolder = undefined;
						return this.element.removeChild( cH );
					}
				} else {
					this.caretHolder = undefined;
				}
			}
			if ( !this.caretHolder && 
				( 
					( this.element.lastChild && this.element.lastChild.nodeName === "BR" )
					//|| !this.element.lastChild // Handle IE's not being able to
					// select empty NSTs. This isn't working well.
				)
			) {
				// If we don't have a caretHolder, but we do need it, add it.
				this.caretHolder = document.createElement( "span" );
				this.caretHolder.className = "SWKB-caretHolder";
				this.element.appendChild( this.caretHolder );
			}
		},
		// Probably to be removed/moved to caret or something.
		textOffset: Sign.prototype.textOffset,
		mergeWith: function ( NST ) {
			for ( ; NST.element.firstChild; ) {
				this.element.appendChild( NST.element.firstChild );
				this.normalize();
			}
			signArea.removeSign( NST );
		},
		/**
		 * Splits the NST by the caret.
		 * @return {NonSignText} The latter half of this, if it has content.
		 */
		split: function ( range ) {
			// This function is currently used by togglePlainText and 
			// interpretFSW.
			
			// Does this belong more in the caret object? I'm thinking yes.
			var frag = document.createDocumentFragment(),
				range = range || caret.getRange(),
				NST;
			
			if ( !range.collapsed ) {
				range.deleteContents();
			}
			
			// Multiple options here:
			// 1. Extend range to the end, and extract.
			// 2. Locate range, use TextNode.splitText if necessary, and 
			//    relocate all the later nodes to the new NST.
			// 2.1. Or use range.insertNode to divide it.
			// Maybe do some performance tests.
			
			// Trying option 1.
			if ( this.element.lastChild ) {
				range.setEndAfter( this.element.lastChild );
				frag = range.extractContents();
				if ( this.caretHolder && this.caretHolder.parentNode === frag ) {
					frag.removeChild( this.caretHolder );
				}
				this.maintain();
			}
			
			// Create new NST.
			if ( frag.firstChild ) {
				console.log( frag.childNodes, frag.firstChild );
				NST = new NonSignText( signArea );
				NST.element.appendChild( frag );
				NST.maintain();
				NST.useInnerText();
				if ( NST.isEmpty() ) {
					// oops. NST has no content, shouldn't be added.
					NST = undefined;
				} else {
					signArea.addSignLegacy( NST, this.index() + 1 );
					//NST.useInnerText();
				}
			}
			
			return NST;
		},
		// TODO: Fix duplication. Maybe inherit or something.
		index: Sign.prototype.index
	};
	
	// ** KEYBOARD **
	// The keyboard is a position: fixed div element. Each key is a div element.
	// The images for the keys are background images produced by simulating key
	// presses for each key.
	keyboard = ( function () {
		// TODO: Keyboard settings system. Need to be able to change layouts,
		// language, others, disable keyboard, ...
		
		var currentLanguageData,
			keyMap,
			// keyCode: boolean true if pressed
			keysPressed = {},
			// List of keys' dom nodes.
			keyElements = [],
			iconContainerElements = [],
			keyIconCache = [],
			// Not sure about this one. Maybe make it an exposed property.
			// Actually, maybe use it for a lot of things. Plenty of keys that
			// need suppressing based on map.
			suppressKeyUpdate = {},
			// Whether the right or left shift keys are pressed.
			shiftKeys = {
				"false" : false, // Left shift
				"true" : false   // Right shift
			},
			// Actions to take (once) when a key is released.
			keyupActions = {},
			hidden = false,
			settings,
			// Some browsers return incorrect keyCodes for these keys.
			keyRemapper = {
				59: 186,
				173: 189,
				61: 187
			},
			keyCodeMapper = {
				"Backquote": 0,
				"Digit1": 1,
				"Digit2": 2,
				"Digit3": 3,
				"Digit4": 4,
				"Digit5": 5,
				"Digit6": 6,
				"Digit7": 7,
				"Digit8": 8,
				"Digit9": 9,
				"Digit0": 10,
				"Minus": 11,
				"Equal": 12,
				"KeyQ": 13,
				"KeyW": 14,
				"KeyE": 15,
				"KeyR": 16,
				"KeyT": 17,
				"KeyY": 18,
				"KeyU": 19,
				"KeyI": 20,
				"KeyO": 21,
				"KeyP": 22,
				"BracketLeft": 23,
				"BracketRight": 24,
				"Backslash": 25,
				"KeyA": 26,
				"KeyS": 27,
				"KeyD": 28,
				"KeyF": 29,
				"KeyG": 30,
				"KeyH": 31,
				"KeyJ": 32,
				"KeyK": 33,
				"KeyL": 34,
				"Semicolon": 35,
				"Quote": 36
			},
			bottom,
			initialized = false,
			iconTemplate = ( function () {
				var template = document.createElement( "span" );
				template.className = "SWKB-KBSymbol";
				return template;
			})();
		
		/**
		 * Initialize the keyboard, building the DOM and setting up the relevant
		 * event handlers.
		 */
		function init() {
			if ( initialized ) {
				return false;
			}
			
			initialized = true;
			
			// Get language and keymap data.
			keyboard.currentLanguageData = currentLanguageData = getDefaultLanguage();
			keyboard.keyMap = keyMap = getDefaultKeyMap();
			
			// DOM construction. Set up the element itself.
			keyboard.element = ( function buildElement() {
				// TODO: Investigate flexbox. Could be useful here.
				var keyboardElement = document.createElement( "div" ),
					texts = layout.keyTexts,
					actionsMap = layout.baseKeyboardLayout,
					kbBreaks = [ 0, 13, 26, 37 ],
					// Currently unused, handled by CSS instead.
					kbMargins = [ 0, 70, 90, 110 ];
				
				keyboardElement.className = "SWkeyboard";
				
				var keyTemplate = ( function () {
					var t = document.createElement( "div" ),
						iconContainerElement = t.appendChild( document.createElement( "span" ) ),
						letterIcon = t.appendChild( document.createElement( "span" ) );
						iconContainerElement.className = "SWKB-KBicon";
						letterIcon.className = "SWKB-KBLetter";
					return t;
				})();
				
				for ( var i = 0, div, keyIcon, rightHand, length = keyMap.layout.length; i < length; i++ ) {
					keyIcon = actionsMap[ i ];
					rightHand = actionsMap.indexOf( keyIcon ) !== i;
					keyElements[ i ] = div = keyTemplate.cloneNode( true );
					
					if ( kbBreaks.indexOf( i ) !== -1 ) {
						i && keyboardElement.appendChild( document.createElement( "br" ) );
						// div.style.marginLeft = kbMargins[ kbBreaks.indexOf( i ) ] + "px";
					}
					
					if ( keyIcon === "?" ) {
						div.style.backgroundColor = "#CCC";
					}
					
					iconContainerElements.push( div.firstChild );
					
					var text = texts[ keyIcon ];
					
					// The main container ("div", or keyElements[ i ]), contains
					// three elements, in order:
					// * The span containing the actual symbol when fonts are 
					//   being used. (span.SWKB-KBicon, currently in 
					//   iconContainerElements array.)
					// * The plain text on the key. Plain text node. (Optional.)
					// * The keyboard letter, containened in span.SWKB-KBLetter.
					
					/*
					// Currently unused.
					if ( typeof text === "function" ) {
						text = text( rightHand );
					}
					*/
					
					text && div.insertBefore( document.createTextNode( text ), div.lastChild );
					
					if ( text && text.length === 1 ) {
						div.style.fontSize = "20px";
					}
					
					( function ( i ) {
						div.onmousedown = function () {
							setKeyState( keyMap.layout[ i ], true );
							return false;
						};
						div.onmouseup = function () {
							setKeyState( keyMap.layout[ i ], false );
						};
					} )( i );
					
					keyboardElement.appendChild( div );
				}
				greyLetters();
				
				// Keyboard dragging
				( function moveicon() {
					var div = document.createElement( "div" ), 
						style = keyboardElement.style,
						alignLeft = config.defaultAlignLeft,
						side = alignLeft ? "left" : "right",
						xPos = prefs.get( "kb-" + side ) || 0;
					bottom = prefs.get( "kb-bottom" ) || 0;
					style[ side ] = xPos + "px";
					style.bottom = bottom + "px";
					div.className = "SWKB-moveicon";
					// This is pretty much directly copied from the SignSymbol code. 
					// TODO: Replace with a generic function for both.
					div.onmousedown = function ( e ) {
						style.transition = "none";
						e = e || window.event;
						var startX = e.clientX, startY = e.clientY,
							startBottom = bottom,
							startXPos = xPos;
						var x = alignLeft ? xPos - e.clientX : xPos, 
							y = bottom - e.clientY,
							/*
							// IE swaps these
							w = document.documentElement.clientHeight,
							h = document.documentElement.clientWidth,
							*/
							//w = document.documentElement.clientWidth,
							w = caret.docWidth(),
							h = document.documentElement.clientHeight,
							elW = keyboardElement.offsetWidth,
							elH = keyboardElement.offsetHeight;
						// TODO: Rename.
						function move( e ) {
							e = e || window.event;
							var xDelta = x - ( e.clientX ), 
								yDelta = y - ( e.clientY );
							var newX = e.clientX,
								newY = e.clientY;
							if ( yDelta || xDelta ) {
								style.bottom = ( bottom = Math.max( 0, Math.min( 
									h - elH, startBottom + startY - newY ) ) ) + "px";
								style[ side ] = ( xPos = Math.max( 0, Math.min( 
									w - elW, startXPos - ( startX - newX ) *
										( alignLeft ? 1 :-1 ) ) ) ) + "px";
							}
							return false;
						}
						
						function end() {
							removeEventListener( document, "mousemove", move, false );
							removeEventListener( document, "mouseup", end, false );
							style.transition = "";
							prefs.set( "kb-" + side, xPos );
							prefs.set( "kb-bottom", bottom );
						}
						
						addEventListener( document, "mousemove", move, false );
						addEventListener( document, "mouseup", end, false );
						
						return false;
					};
					//keyboardElement.appendChild( div );
					keyboardElement.insertBefore( div, keyboardElement.firstChild );
				} )();
				
				// Settings menu
				settings = ( function keyboardSettings() {
					var settings = {},
						settingsButton,
						menuElement = document.createElement( "div" );
					menuElement.className = "SWKB-settings-menu";
					
					settingsButton = 
						keyboardElement.appendChild( document.createElement( "div" ) );
					settingsButton.className = "SWKB-settings";
					// This needs to be in sign language, or maybe just an icon.
					// settingsButton.appendChild( document.createTextNode( "Settings" ) );
					settingsButton.onclick = function () {
						function hideMenu ( e ) {
							var target = e.target || e.srcElement;
							for ( ; target && target !== settingsButton; ) {
								target = target.parentNode;
							}
							if ( !target ) {
								menuElement.style.display = "none";
								removeEventListener( document.body, "click", hideMenu );
							}
						}
						menuElement.style.display = "block";
						addEventListener( document.body, "click", hideMenu );
						//return false;
					};
					
					function dontBlur() {
						if ( config.ce ) {
							caret.refocusRange = caret.getRange();
						}
					}
					
					settingsButton.onmousedown = 
						menuElement.onmousedown = 
						dontBlur;
					settingsButton.appendChild( menuElement );
					
					// TODO: Rewrite this entire section. It's a mess.
					function x( label, obj, selected, select ) {
						menuElement.appendChild( document.createTextNode( label + ":" ) );
						for ( var i in obj ) {
							if ( obj.hasOwnProperty( i ) ) {
								var text = obj[ i ].label, option, radio;
								option = menuElement.appendChild( document.createElement( "label" ) );
								radio = document.createElement( "input" );
								radio.type = "radio";
								radio.name = "SWKB-" + label;
								if ( selected( obj[ i ] ) ) {
									radio.checked = true;
								}
								option.appendChild( radio );
								option.appendChild( document.createTextNode( text ) );
								radio.onclick = select( i );
								// Another blur happens after clicking on a selectable
								// element. Might be between mouseup and click, though.
								// Not sure. TODO: Check. Also, check other browsers.
								option.onmouseup = dontBlur;
							}
						}
						menuElement.appendChild( document.createElement( "hr" ) );
					}
					
					x( "Language", languageData, function ( x ) {
						return x === currentLanguageData;
					}, function ( i ) {
						return function () {
							// TODO: Store the pref in cookie or localStorage.
							// If we've got some fancy extension pref storage, use that.
							// Same on the keyboard layout prefs.
							prefs.set( "lang", i );
							keyboard.currentLanguageData = currentLanguageData = languageData[ i ];
							greyLetters( true );
							updateKeyboard();
						};
					} );
					
					x( "Keymap", keyMaps, function ( x ) {
						return x === keyMap;
					}, function ( i ) {
						return function () {
							prefs.set( "keyMap", i );
							keyboard.keyMap = keyMap = keyMaps[ i ];
							greyLetters( true );
							updateKeyboard(); // For fingerspelling.
						};
					} );
					
					x( "Layout", layouts, function ( x ) {
						return x.label === layout.label;
					}, function ( i ) { 
						return function () {
							prefs.set( "layout", layouts[ i ].label );
							changeLayout( layouts[ i ].init );
						};
					} );
					
					// Autocomplete setting
					( function () {
						var l = document.createElement( 'label' ),
							c = document.createElement( 'input' );
						c.type = "checkbox";
						c.checked = prefs.get( 'disableAC' ) ? '' : 'checked';
						c.onchange = function () {
							prefs.set( 'disableAC', !c.checked );
							emit( 'keyAction' );
						};
						l.onmouseup = dontBlur;
						l.appendChild( c );
						l.appendChild( document.createTextNode( 'Autocomplete' ) );
						menuElement.appendChild( l );
					} )();
					
					return settings;
				} )();
					
				return keyboardElement;
			} )();
			
			// Apply key event listeners.
			( function applyListeners() {
				var pendingKeyInput = false;
			
				// Handle events.
				function keyChange( e ) {
					if ( !signArea ) {
						return true;
					}
					
					if ( delegateEvent( e ) === false ) {
						return false;
					}
					
					var keyCode = e.keyCode || e.which, 
						returnValue,
						isSign = activeSign.isSign,
						isKeydown = e.type === "keydown";
					
					if ( keyCode === 16 && ( !e.repeat || !isKeydown ) ) {
						// Set shiftKeys. 
						if ( "location" in e ) {
							shiftKeys[ 
								e.location === KeyboardEvent.DOM_KEY_LOCATION_RIGHT
							] = keyboard.shiftKey = isKeydown;
							if ( e.location === KeyboardEvent.DOM_KEY_LOCATION_STANDARD ) {
								// If we don't know which side it is, do both.
								shiftKeys[ true ] = 
									keyboard.shiftKey = 
									isKeydown;
							}
						} else if ( "shiftLeft" in e ) {
							// Since shiftLeft will be false on keyup even if it's
							// the left shift key, set left shift to unpressed.
							shiftKeys[ false ] = false;
							shiftKeys[ e.shiftLeft === false ] = 
								keyboard.shiftKey = 
								isKeydown;
						} else {
							shiftKeys[ false ] = 
								shiftKeys[ true ] = 
								keyboard.shiftKey =
								isKeydown;
						}
						updateKeyboard();
						
						// console.log( e.shiftLeft, shiftKeys[ false ], shiftKeys[ true ], keyboard.shiftKey );
						
						returnValue = true;
					} else if ( isKeydown ) {
						if ( config.ce ) {
							pendingKeyInput = true;
							// Native keypress event doesn't always fire. 
							setTimeout( function () {
								if ( pendingKeyInput === true ) {
									//keypress();
									postTextAdded();
								}
							}, 16 );
						}
						
						// Ignore if ctrl, alt, meta, or shift are pressed.
						if ( 
							( e.ctrlKey === true || e.metaKey === true ) // && 
							// ( isSign )
						) {
							switch ( keyCode ) {
								case 90: // Ctrl-Z
									// TODO: Deal with NSTs.
									if ( isSign ) {
										e.shiftKey ? 
											signArea.history.redo() : 
											signArea.history.undo();
										// return false;
										returnValue = false;
									} else {
										// Don't touch SignHistory when NST is 
										// focused. 
										returnValue = true;
									}
									break;
								case 89: // Ctrl-Y
									if ( isSign ) {
										signArea.history.redo();
										returnValue = false;
									} else {
										returnValue = true;
									}
									break;
								// Much of the following will need to also work
								// for NSTs. The condition above excepts NSTs.
								case 67: // Ctrl-C
								case 45: // Ctrl-Insert
									// This will be redundant to oncopy in 
									// certain browsers...
									returnValue = caret.copyFSW( e );
									break;
								case 86: // Ctrl-V
									returnValue = caret.pasteFSW( e );
									break;
								case 88: // Ctrl-X
									// TODO.
									returnValue = true;
									break;
								default:
									returnValue = true;
							}
						} else if( // PROBLEM: This shouldn't suppress when 
							// the activeSign is instanceof NonSignText.
							(
								e.ctrlKey !== true && 
								e.altKey !== true && 
								e.metaKey !== true
							) ||
							!isSign
							// e.shiftKey !== true &&
						) {
							returnValue = setKeyState( keyCode, true, e );
						}
						
						postEditProcess();
					} else {
						// Key up
						returnValue = setKeyState( keyCode, false, e );
					}
					
					if ( returnValue === false ) {
						if ( e.preventDefault ) {
							e.preventDefault();
						} else {
							e.returnValue = false;
						}
					}
					
					return returnValue;
				}
				
				// Replacing keypress.
				// Some or all of this doesn't always/ever need a setTimeout 
				// delay. Hmm...
				// Okay, split things into post-edit and post-nativekeyprocess
				// (with setTimeout for the latter).
				// Maybe hook oninput into the latter?
				function postEditProcess() {
					//return;
					/*
					// Maybe in postTextAdded instead?
					if ( config.ce ) {
						caret.check();
					}
					*/
					if ( activeSign.isSign ) {
						signArea.updateTextboxContent();
					} else {
						// activeSign.maintain();
					}
				}
				
				function postTextAdded() {
					pendingKeyInput = false;
					if ( config.ce && activeSign ) {
						if ( activeSign.isSign ) {
							if ( activeSign.ce.firstChild ) {
								// This is quite possibly slower than ideal. ACE uses
								// textnode.value = (placeholder) instead.
								// This gets called on every keypress. 
								// Need to consider performance. TODO.
								activeSign.ce.removeChild( activeSign.ce.firstChild );
							}
						} else {
							if ( signArea ) {
								signArea.updateTextboxContent();
							} else {
								// ???
							}
							
							// Isn't working, and probably isn't necessary. Only sign switches
							// really require an update, except for when a selection is 
							// highlighted.
							if ( activeSign && !activeSign.isSign ) {
								caret.check(); // Not sure
								// Note that check may change the activeSign.
								caret.textBoxSync( true );
								activeSign.maintain && activeSign.maintain();
							}
						}
					}
				}
				
				function oninput() {
					if ( pendingKeyInput === true ) {
						//keypress();
						postTextAdded();
					}
				}
				
				addEventListener( document, "keydown", keyChange, false );
				addEventListener( document, "keyup", keyChange, false );
				addEventListener( document, "input", oninput, false );
				
			} )();
			
			document.body.appendChild( keyboard.element );
		}
		
		// TODO: Consider moving both of these inside init.
		/**
		 * Add a grey letter to the bottom-left corner of each key on the
		 * keyboard.
		 * @param {Boolean} removeOld Remove pre-existing grey letters.
		 */
		function greyLetters( removeOld ) {
			for ( var i = keyElements.length, letter; i--; ) {
				letter = keyElements[ i ].lastChild;
				if ( removeOld ) {
					letter.removeChild( letter.firstChild );
				}
				letter.appendChild( document.createTextNode( 
					getLetter( i )
				) );
			}
		}
		
		// I hate this function name. 
		function updateKeyElement( thisKey, rightHand, symbol, position, override ) {
			// To consider, especially after the truetype switchover: recording
			// the last contents of each key, and only doing a DOM update when
			// necessary.
			
			// For truetype: How to fill in the keys? 
			// Option 1: Find sizes, position appropriately.
			// Option 2: Text-align: center, and position: absolute the white.
			// ...
			
			var action = layout.baseKeyboardActions[ thisKey ],
				keyImageOverride,
				image;
				
			// Issue: Sometimes we're calling the wrong action here.
			//
			// If the key only appears once, then it's a "lefthand"
			// key, but it will be updated with the righthand keys as
			// well.
			// If position === i, then technically, we're dealing with
			// a lefthand symbol no matter what rightHand says.
			// If position === i && rightHand, then we need to use
			// activeSymbols[ false ] instead of symbol.
			// Or, if position === i, use activeSymbols[ false ].
			//
			// activateKey gives priority to the left hand.
			// First checks the lefthand symbol's map, and only if the 
			// key is righthand there does it switch to rightHand map.
			//
			// Note: Much of the above will be false if I ever fix this
			// to run i backwards instead of using lastIndexOfs.
			if ( override ) {
				keyImageOverride = override( position );
			} else {
				symbol.clone( fakeSymbol );
				
				if ( action ) {
					keyImageOverride = 
						action.call( fakeSymbol, rightHand, position, thisKey, shiftKeys[ rightHand ] );
				}
			}
			/*
			// This is a huge mess. TODO: Rewrite.
			if ( keyImageOverride !== undefined ) {
				// Setting the key's image to a sign given by a return value.
				
				// TODO: Set "url("+signServer earlier, so we don't have to do it every time.
				if ( config.useFont ) {
					image = keyImageOverride;
				} else {
					image = keyImageOverride && "url(" + signServer + keyImageOverride + ")";
				}
			} else {
				// Setting the key's image to a single symbol
				
				fakeSymbol.updateImage();
				
				image = fakeSymbol.symbolText;
				if ( image ) {
					if ( config.useFont ) {
						var iconContainer = iconContainerElements[ position ],
							icon;
						
						setNum( 1, iconContainer );
						
						( icon = iconContainer.firstChild )
							.setAttribute( "data-sw-symbol", fontHandler.code( image ) );
						// Center
						fakeSymbol.setDimensions();
						icon.style.left = 25 - widthCache[ image ]  / 2 + "px";
						icon.style.top  = 40 - heightCache[ image ] / 2 + "px";
						return;
					} else {
						image = imgStart + image + end;
					}
				}
			}
			*/
			// ----- REWRITE
			
			if ( keyImageOverride === undefined ) {
				fakeSymbol.updateImage();
				image = fakeSymbol.symbolText;
			}
			
			// false means suppress update.
			if ( keyImageOverride !== false ) {
				if ( keyImageOverride && keyImageOverride.length === 6 ) {
					setKeyImageToFSW( position, keyImageOverride );
				} else {
					setKeyImageToFSW( position, image, keyImageOverride );
				}	
			}
			// ----- /REWRITE
			/*
			// if ( fakeSymbol.symbolText && fakeSymbol.symbolText !== symbol.symbolText ) {
			if ( image ) { // TODO: Allow overriding imgStart/end.
				//keyElement.style.backgroundImage = imgStart + image + end;
				if ( config.useFont ) {
					var i = 0, ex, symbs = [], 
						iconContainer = iconContainerElements[ position ],
						min = 500, max = +image.substr( 5, 3 );
					for ( ; ( ex = simpleSymbolReg.exec( image ) ); i++ ) {
						symbs.push( ex[ 0 ] );
						min = Math.min( min, +ex[ 0 ].substr( 10, 3 ) );
					}
					setNum( i, iconContainer );
					for ( ; i--; ) {
						ex = symbolReg.exec( symbs[ i ] );
						
						( icon = iconContainer.childNodes[ i ] )
							.setAttribute( "data-sw-symbol", fontHandler.code( ex[ 1 ] ) );
						icon.style.left = 25 + ( ex[ 2 ] - 500 ) + "px";
						icon.style.top  = 40 + ( ex[ 3 ] - ( min + max ) / 2 ) + "px";
					}
				} else {
					keyElement.style.backgroundImage = image;
				}
				//keyElement.style.backgroundImage = signServer + image + end;
			} else {
				if ( config.useFont ) {
					setNum( 0, iconContainerElements[ position ] );
				} else {
					keyElement.style.backgroundImage = "";
				}
			}
			*/
		}
		
		// This function assumes that fakeSymbol has the right dimensions :(
		// To consider: Having this function directly set fakeSymbol to the 
		// image when necessary. Maybe build something into updateImage to cache
		// updates?
		// Arg naming is bad. TODO: Fix.
		function setKeyImageToFSW( position, image, keyImageOverride, size ) {
			var imgStart = "url(" + symbolServer /* + "M450x450" */ /* + "S" */, 
				end = /* "500x500" + */ ")",
				keyElement = keyElements[ position ];
			
			if ( config.useFont ) {
				
				function setNum( num, iconContainer ) {
					var ch = iconContainer.childNodes;
					for ( ; ch.length < num; ) {
						iconContainer.appendChild( iconTemplate.cloneNode( true ) );
					}
					for ( ; ch.length > num; ) {
						iconContainer.removeChild( iconContainer.lastChild );
					}
				}
				
				function setImage( node, top, left, image ) {
					node.setAttribute( 
						"data-sw-symbol", 
						fontHandler.code( image )
					);
					//node.style.top  = 40 + top  + "px";
					//node.style.left = 25 + left + "px";
					node.style.cssText  = 
						/* "top:" + ( 40 + top ) + "px;left:" + ( 25 + left ) + "px"; */
						"top:" + ( 40 + top ) * 2 + "%;left:" + ( 25 + left ) * 2 + "%";
				}
				
				var iconContainer = iconContainerElements[ position ];
				
				if ( keyImageOverride !== undefined ) {
					image = keyImageOverride;
					if ( image === keyIconCache[ position ] ) {
						return;
					}
					keyIconCache[ position ] = image;
					var i = 0, ex, symbs = [], 
						min = 500, max = +image.substr( 5, 3 );
					
					i = fswParser.getAllSymbols( image, function ( symbolText ) {
						symbs.push( symbolText );
						min = Math.min( min, +symbolText.substr( 10, 3 ) );
					} );
					
					setNum( i, iconContainer );
					for ( ; i--; ) {
						ex = symbolReg.exec( symbs[ i ] );
						setImage( 
							iconContainer.childNodes[ i ],
							( ex[ 3 ] - ( min + max ) / 2 ),
							( ex[ 2 ] - 500 ),
							ex[ 1 ]
						);
					}
				} else {
					if ( keyIconCache[ position ] === image ) {
						return;
					}
					keyIconCache[ position ] = image;
					if ( image ) {
						// Center
						heightCache[ image ] || fakeSymbol.setDimensions();
						
						setNum( 1, iconContainer );
						
						setImage( 
							iconContainer.firstChild, 
							-heightCache[ image ] / 2,
							-widthCache[ image ]  / 2,
							image
						);
					} else {
						setNum( 0, iconContainer );
					}
				}
			} else {
				if ( fontHandler.loaded ) {
					if ( keyImageOverride !== undefined ) {
						image = keyImageOverride;
						if ( image === keyIconCache[ position ] ) {
							return;
						}
						keyIconCache[ position ] = image;
						var i, ex, symbs = [], 
							minH = 500, maxH = +image.substr( 5, 3 ),
							minW = 500, maxW = +image.substr( 1, 3 ),
							result = "";
						
						i = fswParser.getAllSymbols( image, function ( symbolText ) {
							symbs.push( symbolText );
							minH = Math.min( minH, +symbolText.substr( 10, 3 ) );
							minW = Math.min( minW, +symbolText.substr( 6, 3 ) );
						} );
						
						for ( ; i--; ) {
							ex = symbolReg.exec( symbs[ i ] );
							result += "url(" + fontHandler.getImage( ex[ 1 ] ) + ") no-repeat ";
							result += ( ex[ 2 ] - ( minW + maxW ) / 2 ) + 25 + "px ";
							result += ( ex[ 3 ] - ( minH + maxH ) / 2 ) + 25 + "px";
							if ( i > 0 ) {
								result += ", ";
							}
							keyElement.style.background = result;
						}
					} else {
						heightCache[ image ] || fakeSymbol.setDimensions();
						if ( image === keyIconCache[ position ] ) {
							return;
						}
						keyIconCache[ position ] = image;
						image = image && 'url(' + fontHandler.getImage( image ) + ')';
					}
				} else {
					if ( keyImageOverride !== undefined ) {
						image = keyImageOverride && "url(" + signServer + keyImageOverride + ( size ? '&size=' + size : '' ) + ")";
					} else {
						image = image && imgStart + image + end;
					}
				}
				
				// Problem: This removes the gray background of null keys.
				// TODO: Fix.
				keyElement.style.background = ( image || "" ) + " no-repeat center";
			}
		}
		
		/**
		 * Update the appearance/images of keys on the keyboard.
		 * @param {Boolean} [rightHand] Which side of the keyboard to update. If
		 *  undefined, both sides are updated.
		 */
		function updateKeyboard( rightHand ) {
			// if ( activeSign instanceof NonSignText ) {
			if ( rightHand !== undefined && !activeSymbols[ rightHand ] ) {
				// Maybe collapse the keyboard?
				return;
			}
			
			if ( rightHand === undefined ) {
				updateKeyboard( true );
				updateKeyboard( false );
				return;
			}
			
			var symbol = activeSymbols[ rightHand ],
				// type = symbol.type, //activeSign.lane !== "" ? symbol.type : symbolTypes.PUNCTUATION,
				map = layout.modeMap( symbol ),
				override;
			
			var done = { "" : true, "?" : true };
			
			
			// Okay, this is still a serious mess. TODO: Rewrite.
			if ( layout.keyboardOverrideDisplay ) {
				override = layout.keyboardOverrideDisplay();
				if ( override ) {
					for ( var i = 0; i < keyElements.length; i++ ) {
						updateKeyElement( 
							null, // thisKey
							null, // keyRightHand
							fakeSymbol, // activeSymbols[ keyRightHand ]
							i,
							override
						);
					}
					return;
				}
			}
			
			for( var i = 0, thisKey, position; i < map.length; i++ ) {
				thisKey = map[ i ];
				if ( ( done[ thisKey ] || layout.imagelessKeysObject[ thisKey ] ) && !override ) {
					continue;
				}
				done[ thisKey ] = true;
				
				if ( rightHand ) {
					// Instead of doing this, I should just run i the other
					// direction from the start.
					position = map.lastIndexOf( thisKey );
					/*
					if ( position === i ) {
						continue;
					}
					*/
				} else {
					position = i;
				}
				
				if ( /* position !== -1 && */ !suppressKeyUpdate[ position ] || override ) {
					var keyElement = keyElements[ position ],
						keyRightHand = position !== i;
					
					/*
					// If single-key lefthand keys no longer get updated with
					// the righthand keys, uncomment this.
					if ( 
						( thisKey === "!" && !activeSign.isEmpty() )
					) {
						// TODO: Set up dedicated images or text.
						keyElement.style.backgroundImage = "";
						continue;
					}
					*/
					
					if ( !keyElement ) {
						continue;
					}
					
					
					// updateKeyElement( thisKey, keyElement, rightHand, symbol, position );
					updateKeyElement( 
						thisKey, 
						keyRightHand, 
						activeSymbols[ keyRightHand ], 
						position
					);
				}
			}
		}
		
		// Update a key's status of being pressed or not, and activate keys if necessary.
		// TODO: Split this function. Could be like 
		// if ( setKeyState() ) { run key result stuff }
		// Not sure if that makes sense.
		// Actually, probably shouldn't do that.
		//
		// Return false to suppress key.
		/**
		 * @param {Number} keyCode
		 * @param {Boolean} pressed
		 * @param {Event} event
		 */
		function setKeyState( keyCode, pressed, event ) {
			
			// Extra plainText stuff doesn't really fit here. Move somewhere else?
			var isSign = activeSign.isSign,
				extras = ( isSign || !config.ce ) ? extraKeys : plainTextExtraKeys;
			
			/*
			if ( !isSign && document.activeElement !== activeSign.element ) {
				activeSign.focus();
			}
			*/
			// FF doesn't handle keyCodes on nonstandard keys, sets to 0.
			// Instead, use FF's event.code, which has the additional benefit of
			// not being layout-dependent.
			keyCode = keyboard.keyMap.layout[ keyCodeMapper[ event && event.code ] ] || keyCode;
			
			// Certain browsers give wrong keyCode results.
			keyCode = keyRemapper[ keyCode ] || keyCode;
			keyCode = keyboard.keyMap.remapper && 
				keyboard.keyMap.remapper[ keyCode ] || keyCode;
			
			// TODO: Check whether the keyboard's on (Ctrl-M).
			
			var position = keyMap.layout.indexOf( '' + keyCode );
			
			if ( position === -1 && !( keyCode in extras ) ) {
				return true;
			}
			
			if ( pressed && extras[ keyCode ] ) {
				// TODO: Fix this so that it's the other way around.
				// Is this done?
				return extras[ keyCode ]( keyCode, event ) === true ? true : false;
			}
			
			if ( !isSign ) {
				return true;
			}
			
			if ( pressed && keysPressed[ keyCode ] !== true ) {
				// Move to activateKey, maybe?
				signArea && signArea.history.pushState();
				activateKey( position, event );
			}
			
			emit( "keyAction" );
			
			keysPressed[ keyCode ] = pressed;
			
			if ( position !== -1 ) {
				keyElements[ position ].className = pressed ? "pressed" : "";
			}
			
			if ( !pressed && keyupActions[ position ] ) {
				keyupActions[ position ]();
				delete keyupActions[ position ];
			}
			
			// default to true?
			if ( config.ce ) {
				return true;
			} else {
				return false;
			}
		}
		
		function activateKey( position, event ) {
			
			var map, rightHand, symb, action;
			do { // Assume rightHand = false, go again with true if it doesn't work.
				// This code is hard to read. TODO: Clean up a bit.
				rightHand = !!map; // Check if we're on our first time around.
				map = layout.modeMap( activeSymbols[ rightHand ] /*.type*/ );
				symb = map[ position ];
			} while ( !rightHand && map.indexOf( symb ) !== position )
			
			if ( layout.keyboardOverrideActivate && layout.keyboardOverrideActivate( symb, position ) ) {
				return;
			}
			
			if ( symb && ( action = layout.baseKeyboardActions[ map[ position ] ] ) ) {
				// rightHand = map.indexOf( symb ) !== position;
				// TODO: Throw in some stuff here so that Ctrl-Z can work.
				// Ctrl-M will probably be to turn off keyboard.
				
				action.call( activeSymbols[ rightHand ], rightHand, position, map[ position ], shiftKeys[ rightHand ] );
				if ( !layout.suppressUpdateKeysObject[ map[ position ] ] ) {
					activeSymbols[ rightHand ].updateImage();
					keyboard.update( rightHand );
				}
			}
		}
		
		/**
		 * Toggle whether the text on a key is displayed.
		 * @param {Number} key Position of the key on the keyboard.
		 * @param {Boolean} show Whether the text is visible.
		 */
		function toggleKeyText( key, show ) {
			var keyElement = keyElements[ key ];
			if ( keyElement ) {
				keyElement.style.color = show ? "" : "transparent";
			}
		}
		
		function toggleAllKeyTexts( show ) {
			var keyTexts = layout.keyTexts,
				baseKeyboardLayout = layout.baseKeyboardLayout;
			//for ( var i = 0; i < keyElements.length; i++ ) {
			for ( var i = 0; i < baseKeyboardLayout.length; i++ ) {
				if ( keyTexts[ baseKeyboardLayout[ i ] ] ) {
					toggleKeyText( i, show );
				}
			}
		}
		
		/**
		 * Retrieve the letter at a certain keyboard position, based on current
		 *  language and keyboard settings.
		 * @return {String} letter
		 */
		function getLetter( position ) {
			var keyCode = keyMap.layout[ position ],
				letter = ( currentLanguageData.letterMap && 
					currentLanguageData.letterMap[ keyCode ] ) || 
					( keyMap.letterMap && 
					keyMap.letterMap[ keyCode ] ) || 
					keyCodeTable[ keyCode ];
			return letter;
		}
		
		// Maybe move up.
		/**
		 * Determine probable language to default to.
		 * @return {Object} data Collection of data from the languageData object.
		 */
		function getDefaultLanguage() {
			// TODO: Search for sign languages in lang attributes first.
			
			// First, check if we have a direct override from config.
			if ( config.useLang ) {
				return languageData[ config.useLang ];
			}
			
			// Second, check the page content.
			// Could be problematic during the incubator phase...
			/*
			if ( document.body && languageData[ document.body.lang ] ) {
				return languageData[ document.body.lang ];
			}
			*/
			
			// Third, check if there's a preference set in cookies or elsewhere.
			// TODO.
			if ( prefs.get( "lang" ) ) {
				return languageData[ prefs.get( "lang" ) ];
			}
			
			// Fourth, take a guess, using browser language prefs.
			// Consider using String.indexOf to compare.
			var language = navigator.language || navigator.userLanguage;
			for ( var i in languageData ) {
				if ( languageData[ i ].localeGuesses.indexOf( language ) !== -1 ) {
					return languageData[ i ];
				}
			}
			if ( navigator.languages ) {
				for ( var l = navigator.languages.length; l--; ) {
					language = navigator.languages[ l ];
					// Duplication. TODO: Fix.
					for ( var i in languageData ) {
						if ( languageData[ i ].localeGuesses.indexOf( language ) !== -1 ) {
							return languageData[ i ];
						}
					}
				}
			}
			return languageData.ase;
		}
		
		function getDefaultKeyMap() {
			return keyMaps[ prefs.get( "keyMap" ) ] ||
				keyMaps[ currentLanguageData.keyMap ] ||
				keyMaps[ config.defaultKeyMap ];
		}
		
		/**
		 * Color a key on the keyboard.
		 * @param {Number} position
		 * @param {String} color
		 */
		function colorKey( position, color ) {
			var style = keyElements[ position ].style;
			if ( "boxShadow" in style ) {
				style.boxShadow = color ? color + " 0 0 15px inset" : "";
			} else {
				style.backgroundColor = color ? color : "";
			}
		}
		
		/**
		 * Show the keyboard.
		 */
		function show() {
			if ( hidden === true ) {
				hidden = false;
				keyboard.element.style.bottom = bottom + "px";
			}
		}
		
		/**
		 * Hide the keyboard.
		 */
		function hide() {
			if ( hidden === false ) {
				hidden = true;
				keyboard.element.style.bottom = "-220px";
			}
		}
		
		/**
		 * Clear the images from all of the keys.
		 */
		function blank() {
			for ( var i = 0; i < keyElements.length; i++ ) {
				keyIconCache[ i ] = "";
				if ( config.useFont ) {
					for ( ; iconContainerElements[ i ].firstChild; ) {
						iconContainerElements[ i ].removeChild( 
							iconContainerElements[ i ].firstChild
						);
					}
				} else {
					keyElements[ i ].style.backgroundImage = "";
				}
			}
		}
		
		function rewrite() {
			/*
			 * This entire thing is loaded with tons of duplication of init. In
			 * serious need of cleanup. TODO.
			 */
			
			var texts = layout.keyTexts,
				actionsMap = layout.baseKeyboardLayout;
			
			for ( var i = 0, div, keyIcon, old, length = keyMap.layout.length; i < length; i++ ) {
				keyIcon = actionsMap[ i ];
				div = keyElements[ i ];
				
				div.style.backgroundColor = keyIcon === "?" ? "#CCC" : "#FFFFFF";
				
				var text = texts[ keyIcon ];
				
				old = div.lastChild && div.lastChild.previousSibling;
				
				if ( old && old.nodeType === 3 ) {
					div.removeChild( old );
				}
				
				if ( text ) {
					div.insertBefore( 
						document.createTextNode( text ), 
						div.lastChild
					);
					
					div.style.fontSize = text.length === 1 ? "20px" : undefined;
				}
			}
			greyLetters( true );
		}
		
		//init();
		
		return {
			init: init,
			keyMap : keyMap,
			update: updateKeyboard,
			keyupActions: keyupActions,
			setKeyState: setKeyState,
			toggleKeyText: toggleKeyText,
			toggleAllKeyTexts: toggleAllKeyTexts,
			setKeyImageToFSW: setKeyImageToFSW,
			element: undefined,
			show: show,
			hide: hide,
			blank: blank,
			rewrite: rewrite,
			getLetter: getLetter,
			shiftKeys: shiftKeys,
			shiftKey: false,
			currentLanguageData : currentLanguageData,
			colorKey: colorKey
		};
	} )();
	
	// This might be named wrong. I'm not completely sure what "emit" means...
	// If I ever make a layoutHandler object, this might be in it.
	// the arg#s args should be replaced, but I'd like to avoid an unnecessary
	// .slice.call( arguments, 1 ) here.
	// Current hooks: 'space', 'changeSign', 'keyAction'
	function emit( x, arg1, arg2 ) {
		var funct = layout.hooks && layout.hooks[ x ];
		if ( funct ) {
			funct( arg1, arg2 );
		}
	}
	
	// ** ASSORTED EXTRA BITS **
	
	
	// TODO: Rewrite much of this so that it's isolated, not doing any of the
	// direct fancy work.
	var autocomplete = ( function () {
		
		var preSuggestion = false,
			lastSuggestion = false;
		
		return {
			query: function ( query, callback ) {
				try {
					var req = new XMLHttpRequest();
					req.open( "GET", "https://swserver.wmflabs.org/puddle/" +
						keyboard.currentLanguageData.code +
						"/query/S/" + query + "?limit=" + config.numberOfSuggestions );
					req.onreadystatechange = function () {
						if ( req.readyState === 4 ) {
							var responseObject;
							try {
								responseObject = JSON.parse( req.response );
							} catch ( e ) {
								// malformed response
								return;
							}
							callback( responseObject );
						}
					};
					req.send();
				} catch ( e ) {
					// Probably an "access denied" bit over the cross-domain stuff.
				}
			},
			findSuggestions: function ( callback ) {
				// Find some suggestions, display one, and pass the (formatted) 
				// results from the server to the callback.
				var currentSignText = activeSign.signText,
					queryString = activeSign.symbols.join( "" ),
					lastIndex = lastSuggestion && lastSuggestion.indexOf( currentSignText ),
					disabled = prefs.get( 'disableAC' );
				//console.log( "prepare query:", "Q" + queryString );
				//console.log( "https://swserver.wmflabs.org/puddle/sgn4/query/Q" + queryString );
				
				if ( queryString && ( lastIndex === false || lastIndex === -1 ) && !disabled ) {
					//autocomplete.query( "Q" + queryString, function ( response ) {
					autocomplete.query( currentSignText, function ( response ) {
						
						var suggestions;
						
						if ( response.meta.totalResults > 0 ) {
							var results = response.results, 
								alreadyAdded = {};
							suggestions = [];
							for ( var i = 0, l = results.length; i < l; i++ ) {
								var suggestion = results[ i ].sign;
								// Don't include duplicate suggestions.
								if ( !alreadyAdded[ suggestion ] ) {
									suggestions.push( suggestion );
									alreadyAdded[ suggestion ] = true;
								}
							}
						} else {
							suggestions = false;
						}
						callback( lastSuggestion = suggestions );
					} );
				} else {
					callback( lastSuggestion = false );
				}
			},
			// Maybe move this to sign.prototype?
			// Unused.
			removePseudos: function () {
				var pseudos = activeSign.pseudos;
				for ( var i = 0; i < pseudos.length; i++ ) {
					if ( pseudos[ i ].pseudoCat === "AC" ) {
						pseudos.splice( i--,  1 )[ 0 ].removeElement();
					}
				}
			},
			// Unused.
			showSuggestion: function ( suggestion ) {
				autocomplete.removePseudos();
				if ( suggestion ) {
					/*
					for ( var reg = simpleSymbolReg, r; r = reg.exec( suggestion ); ) {
						var pseudo = new PseudoSymbol( r[ 0 ], "77DDFF" );
						pseudo.pseudoCat = "AC";
						activeSign.pseudos.push( pseudo );
					}
					*/
					
				}
			},
			useSuggestion: function ( suggestion ) {
				//console.log( "useSuggestion", suggestion );
				// Direct override, which is really non-ideal, but otherwise we have
				// issues while we wait for the dimensions to load.
				activeSign.signText = suggestion;
				
				// TODO: Merge this with the duplicate in Sign()
				activeSign.spellingSequence = "";
				suggestion = suggestion.replace( 
					spellingSequenceReg, 
					function( ss ) {
						activeSign.spellingSequence = ss;
						// Afterwards, strip them out.
						return "";
					}
				);
				
				for ( var i = activeSign.symbols.length, symb; i--; ) {
					symb = activeSign.symbols[ i ];
					if ( symb.type !== symbolTypes.NONE ) {
						symb.remove();
					}
				}
				var oldText = activeSign.signText;
				
				fswParser.getAllSymbols( suggestion, function ( symbolText ) {
					var newSymbol = new SignSymbol( symbolText, activeSign );
					activeSign.symbols.push( newSymbol );
					newSymbol.useSymbolFSW();
					newSymbol.updateImage();
					newSymbol.buildElement( true );
				} );
				
				activeSign.focusSymbols();
			},
			suggestLoop: function ( recallPre ) {
				var currentSignText = activeSign.toString(),
					ind = lastSuggestion && lastSuggestion.indexOf( currentSignText ),
					len = lastSuggestion && lastSuggestion.length,
					dir = keyboard.shiftKey ? -1 : 1,
					targetInd = ind + dir;
				
				if ( !lastSuggestion ) {
					return '';
				}
				
				if ( ind === -1 && recallPre ) {
					preSuggestion = currentSignText;
				}
				
				if ( targetInd === -2 ) {
					// Loop around backward.
					targetInd = len - 1;
				}
				
				return lastSuggestion[ targetInd ] || preSuggestion;
			},
			hasSuggestions: function ( text ) {
				return lastSuggestion && lastSuggestion.indexOf( text ) !== -1;
			}
		};
	} )();
	
	// This is not, strictly speaking, irrelevant to all other possible layouts.
	// Keeping this available makes sense, I think. Or maybe just parts?
	// (That is, the components that make use of handHeels and 
	// HandBetweenFacings.)
	function handSymbolExtras( rightHand, symbol, image ) {
		
		/*
		// No longer using handBetweenFacings.
		if ( handBetweenFacings[ image ] && symbol.face % 1 ) {
			// image += ( symbol.plane * 2 + ( symbol.face & 1 ) );
			image = ( 
				handBetweenFacings[ image ] + 
				(
					( symbol.plane * 2 ) + 
					( symbol.face === 2.5 || symbol.face === 1.5 )
				) +
				(
					( symbol.rightHand ^ ( symbol.face > 2 ) ? symbol.rotation : ( 8 - symbol.rotation ) % 8 ) +
					( ( symbol.face < 2 ) !== symbol.rightHand ? 8 : 0 )
				).toString( 16 )
			);
		} else {
		*/
		if ( handHeels.indexOf( image ) !== -1 ) {
			image += "1";
		} else {
			image += ( symbol.plane * 3 + ( symbol.face >= 3 ? 1 : symbol.face | 0 ) );
		}
		rightHand ^= symbol.face >= 3;
		image += (
			( rightHand ? 0 : 8 ) + 
			( rightHand ? symbol.rotation : ( 8 - symbol.rotation ) % 8 ) 
		).toString( 16 );
		return image;
	}
	
	// TODO: Consider merging this with 'generalActions' at the top.
	function toggleFingerSpelling( rightHand, position, alt ) { // Activate fingerspelling mode
		if ( this.fake ) {
			// keyboard.toggleKeyText( position, false );
			return i18n.ase.fingerspell;
		}
		var x = !activeSign.mode,
			actionsMap = layout.baseKeyboardLayout;
		// Not sure of FS data should be removed when turning off.
		//
		// ... set it just below existing signs, maybe?
		activeSign.mode = x ? { 'type': 'fs', Y: 0, letters: [] } : false;
		keyboard.blank();
		/*
		for ( var i = 0; i < actionsMap.length; i++ ) {
			//if ( i !== actionsMap.indexOf( "F" ) ) {
			// if ( actionsMap[ i ] !== alt ) {
			keyboard.toggleKeyText( i, !x );
		}
		*/
		keyboard.toggleAllKeyTexts( !x );
		keyboard.update( true );
	}
	
	// Okay, now that there's a keyboard override for the visible icons, I 
	// should probably set up an equivalent for actual processing. Maybe.
	function fingerspell( position ) {
		var map = keyboard.currentLanguageData,
			letter = keyboard.getLetter( position ),
			FSW = ( keyboard.shiftKey && map.shiftLetters && map.shiftLetters[ letter ] ) || map.letters[ letter ],
			sign = activeSign,
			newSymbols, newSymbol,
			pReg = /[0-9]{3}(?=S|$)/g,
			oldY = sign.mode.Y || 450,
			val, top = 1000;
		
		if ( FSW ) {
			sign.mode.letters.push( newSymbols = [] );
			for ( ; ( val = pReg.exec( FSW ) ); ) {
				top = Math.min( top, val );
			}
			// If I ever develop "MultiSymbol()", use that instead.
			fswParser.getAllSymbols( FSW, function ( symbolText ) {
				newSymbols.push( newSymbol = new SignSymbol( symbolText, sign ) );
				newSymbol.useSymbolFSW();
				newSymbol.updateImage();
				sign.symbols.splice( sign.symbols.length - 2, 0, newSymbol );
				newSymbol.Y += oldY - top;
				newSymbol.buildElement( true );
			} );
			
			if ( newSymbol ) {
				
				activeSymbols[ newSymbol.rightHand ].blur();
				newSymbol.focus();
				
				sign.mode.Y = oldY + ( FSW.substr( 5, 3 ) - top ) + 4;
			}
		}
	}
	
	// Should these three be moved somewhere?
	/**
	 * Returns a function that deletes an active sign, selecting the sign before
	 *  or after it depending on dir. ( 1 = forward, -1 = backward )
	 */
	function deleteSign( dir ) {
		return function () {
			// To consider:
			// What does delete key do in fingerspelling mode?
			// What happens when you backspace the first sign, or delete the 
			// last sign?
			// Or the only sign?
			// 
			
			
			if ( activeSign.mode && activeSign.mode.type === 'fs' && activeSign.mode.letters.length ) {
				// This should probably be moved to a layout hook.
				// Fingerspelling: Remove one letter.
				var fs = activeSign.mode, 
					letter = fs.letters.pop(),
					focusLetter = fs.letters[ fs.letters.length - 1 ],
					symbols = activeSign.symbols, 
					i = 0, index;
				
				for ( ; i < letter.length; i++ ) {
					// BUG: If Ctrl-Z has been used, serious potential for breakage.
					// BUG: If symbol is active, need to deactivate before removing.
					index = symbols.indexOf( letter[ i ] );
					if ( index !== -1 ) {
						activeSign.symbols.splice( index, 1 );
						activeSign.element.removeChild( letter[ i ].element );
						fs.Y = Math.min( fs.Y, letter[ i ].Y );
					}
				}
				
				activeSign.updatePosition();
				
				if ( focusLetter ) {
					focusLetter[ focusLetter.length - 1 ].focus();
				} else {
					// Ah, focus something random.
				}
			} else {
				// Remove one sign.
				var index = activeSign.index(),
					oldSign = activeSign,
					targetSign   = signArea.signs[ index + dir ],
					oppositeSign = signArea.signs[ index - dir ];
				
				// This is in need of cleanup. TODO.
				
				if ( !targetSign ) {
					// TODO: Fill in with a blank sign if there's nothing left.
					// Maybe.
					return;
				}
				
				if ( dir === 1 && !targetSign.isSign && oppositeSign && !oppositeSign.isSign ) {
					// If deleting symbol after the caret, and surrounded by
					// NonSignTexts, merge the two NSTs and place the caret at
					// the point formerly between them.
					signArea.changeSign( oppositeSign, true );
					caret.selectElement( oppositeSign.element, true );
					oppositeSign.mergeWith( targetSign );
				} else {
					signArea.changeSign( targetSign, true );
					if ( !targetSign.isSign ) {
						// Move caret to the end of the NST if going backwards.
						caret.selectElement( targetSign.element, dir === -1 );
						if ( oppositeSign && !oppositeSign.isSign ) {
							// Merge the two NSTs.
							targetSign.mergeWith( oppositeSign );
						}
					}
				}
				signArea.removeSign( oldSign );
				keyboard.update();
			}
		}
	}
	
	function deleteText( dir ) { // Backspace/delete key, on NonSignTexts.
		var forward = dir === 1;
		return function () {
			/*
			* 1. Empty NonSigns should be removed.
			* 2. Selections including other signs should be removed.
			* 3. Carets at the beginning/end of the NST should do trigger a 
			     sign change.
			*/
			
			if ( caret.isCollapsed() ) {
				if ( caret.locate( dir ) ) {
					// There are no characters behind/in front of the caret.
					var index = activeSign.index(),
						sign = activeSign;
					if ( forward ? index < signArea.signs.length - 1 : index > 0 ) {
						// Jump to the previous/following sign, and delete the 
						// NST if empty.
						signArea.changeSign( index + dir );
						if ( sign.isEmpty() ) {
							signArea.removeSign( sign );
						}
					} else {
						// There are no earlier/later signs to jump 
						// back/forward to.
						return false;
					}
				} else {
					caret.midDeletion = true;
					// TODO: If this will cause the NST to go nuts/disappear, do
					// something to prevent it.
					return true;
				}
			} else {
				// We're dealing with a full selection here...
				// Check before saving.
				caret.getRange().deleteContents();
				// Not working?
				signArea.updateFromDOM();
				caret.collapseSelection();
			}
			// Check if backspacing will break adjacent symbol.
		}
	}
	
	/**
	 * Create and focus a new Sign following the currently focused Sign.
	 */
	function spaceKey() {
		// TO CONSIDER: Relocating the centerSign to Sign.blur()
		activeSign.centerSign && activeSign.centerSign();
		// Add a new sign to the list immediately after the current sign.
		var oldSign = activeSign, 
			newSign,
			index = oldSign.index();
		
		activeSign.blur();
		// Should the new sign's lane start with the same as the previous sign?
		// TODO: Ask someone.
		newSign = signArea.addSign( index + 1 );
		
		newSign.focus();
		
		emit( 'space' );
		
		keyboard.update();
	}
	
	
	// ** CARET/SELECTION HANDLING **
	
	// All of this is unused so long as config.ce is set to false.
	// TODO: Consider importing rangy. Could make things easier.
	// TODO: All of the major stuff that needs to be done.
	
	/*
	 * List of range/selection-related stuff:
	 * * Toggling plaintext.
	 * * Focusing signs when switching and otherwise.
	 * * - uTBC>createRange>setStart>setEnd>removeAllRanges>addRange>detach
	 * * Focusing NSTs.
	 * * - createRange>setStart>setEnd>removeAllRanges>addRange>detach
	 * * - (Is missing uTBC)
	 * * Moving the caret around in NSTs, including enter key.
	 * * up/down arrows, without sign change:
	 * * Non-IE: (check focusNode, focusOffset)>modify
	 * * IE: (not checked) getRangeAt>setEnd/Start/EndBefore/After>addRange
	 * * Splitting NSTs.
	 * * Synchronizing the text box's caret with the SignArea.
	 * * - uTBC
	 * * - getRangeAt>?>
	 * * Non-IE: set selectionStart/End/Direction>removeAllRanges>addRange>detach
	 * * IE: ??
	 * * Copy-pasting moves the selection.
	 * * - copyPasteBox.select()
	 * Using removeAllRanges can unfocus the SignArea. When doing this, a blur
	 * event must be suppressed.
	 */
	/*
	 * Current IE issues:
	 * Caret keeps jumping to the beginning when typing on NST.
	 * SA keeps blurring.
	 */
	caret = {
		// BUG: Clicking on the keyboard or SA will blur the SA, via blur event.
		// Note: Mousedown runs before blur does. TODO: Something.
		// TODO: Consider replacing "busy" with "status", including options for
		// "IDLE", "BUSY", "REFOCUS", and maybe some things for awaiting change,
		// and/or some other stuff. Use enum thingy, caret.statuses? Needs 
		// better name than that.
		busy: false,
		refocusRange: false,
		ieSelection: !( "getSelection" in window ),
		/**
		 * Retrieve the Range of the current selection.
		 * @return {Range}
		 */
		getRange: function () {
			if ( window.getSelection ) {
				var selection = window.getSelection(), range;
				if ( selection.rangeCount > 0 ) {
					try {
						return selection.getRangeAt( 0 );
					} catch( e ) {
						return false;
					}
				} else {
					return false;
				}
			} else {
				return document.selection || false;
			}
		},
		/**
		 * Add a range to a selection, without triggering the blur events.
		 * @param {Range} range The range to be selected.
		 * @param {boolean} Whether the range is backward and the direction 
		 *  should be preserved. (Does not work in IE.)
		 */
		selectRange: function ( range, backwards ) {
			caret.busy = true;
			if ( window.getSelection ) {
				var selection = window.getSelection();
				selection.removeAllRanges();
				//console.log( "Removed range" );
				try {
					if ( backwards && selection.extend ) {
						var startContainer = range.startContainer,
							startOffset = range.startOffset;
						range.collapse( false );
						selection.addRange( range );
						selection.extend( startContainer, startOffset );
					} else {
						selection.addRange( range );
					}
				} catch ( e ) {
					console.error( "Addrange broke ", e );
				}
			} else {
				
			}
			caret.busy = false;
		},
		/**
		 * Move the caret to an element, optionally synchronizing the text box
		 * caret at the same time.
		 * @param {HTMLElement} element Element to move the caret to.
		 * @param {boolean} focusEnd Whether the caret should be moved to the 
		 *  end of the element instead of the beginning.
		 * @param {boolean} sync Whether to synchronize the text box caret.
		 */
		selectElement: function ( element, focusEnd, sync ) {
			if ( !element || !element.parentNode || !element.parentNode.parentNode ) {
				return;
			}
			
			if ( caret.ieSelection ) {
				var range = document.body.createTextRange();
				if ( focusEnd ) {
					range.selectElement( element );
					//range.collapse( false );
				} else {
					range.moveToElementText( element );
					range.select();
				}
				return;
			}
			
			// console.log( "selecting element:", element, focusEnd );
			// lots of duplication with above...
			caret.busy = true;
			var selection = window.getSelection(),
				range = document.createRange(); //selection.getRangeAt( 0 );
			if ( focusEnd !== true || !element.lastChild ) {
				range.setStart( element, 0 );
				range.setEnd( element, 0 );
			} else {
				range.setStartAfter( element.lastChild );
				range.setEndAfter( element.lastChild );
			}
			
			if ( sync && signArea.textBox ) {
				// Move the caret to the right place in the textbox. Useful
				// for removing an unnecessary caret relocation.
				// That would be: Old>New>TextBox>New. Instead: Old>Textbox>New.
				caret.textBoxSync();
			}
			
			caret.selectRange( range );
			range.detach();
			// console.log( "Resulting focusNode: ", getSelection().focusNode );
		},
		/**
		 * Check if the selection is collapsed.
		 * @return {boolean}
		 */
		isCollapsed: function () {
			return caret.ieSelection ? 
				// This can't be the best way, can it?
				document.selection.createRange().text === "" :
				window.getSelection().isCollapsed;
		},
		collapseSelection: function () {
			return caret.ieSelection ?
				false :
				window.getSelection().collapseToStart();
		},
		/**
		 * Synchronize the caret/selection position of the textbox to the
		 * SignArea's caret position.
		 * @param {boolean} reselect Return the caret to its original position
		 *  after synchronizing the text box.
		 */
		textBoxSync: function ( reselect ) {
			
			if ( !signArea || !signArea.textBox || !config.ce ) {
				return;
			}
			// console.log( "textBoxSync" );
			var box = signArea.textBox, 
				// signIndex = signArea.signs.indexOf( activeSign ),
				signOffset = activeSign.textOffset(),
				/*
				signOffset = 
					fswParser.processMixedText( 
						signArea.signs.slice( 0, signIndex ).join(" ")
					).length + ( signIndex > 0 ),
				*/
				range,
				isBackward;
			
			if ( reselect ) {
				caret.busy = true;
				range = caret.getRange();
			}
			
			if ( !activeSign.isSign ) {
				var selection = window.getSelection(),
					// Check for selection.rangeCount first?
					range,
					signElem = activeSign.element,
					signOffset,
					selectionStart, selectionEnd, 
					focusNode = selection.focusNode, 
					focusOffset = selection.focusOffset;
				
				
				selectionStart = !activeSign.isSign ? 
					signOffset + caret.offset( selection.anchorNode, signElem, selection.anchorOffset ) : 
					signOffset;
				
				if ( selection.collapsed ) {
					selectionEnd = selectionStart;
				} else {
					// Currently not set up to deal with cross-sign selections.
					selectionEnd = signOffset + caret.offset( focusNode, signElem, focusOffset );
				}
				
				if ( selection.rangeCount > 0 ) {
					// perhaps unnecessary
					range = selection.getRangeAt( 0 );
				} else {
					// TODO
					return;
				}
			} else {
				selectionStart = selectionEnd = signOffset;
			}
			
			isBackward = selectionStart > selectionEnd;
			//console.log( "isBackward: ", isBackward, selectionStart, selectionEnd, focusOffset );
			
			// Selections can span multiple signs. TODO: Fix.
			// Simple splice is one letter off when on a sign on nonsign right after
			// a space-removing character. TODO: Fix, preferably efficiently.
			
			//console.log( "SYNCHRONIZING", activeSign.element, selectionStart );
			
			if ( box.nodeName === "TEXTAREA" || box.nodeName === "INPUT" ) {
				
				// box.focus();
				// Use setSelectionRange instead?
				// NOTE: IE and Chrome handle this differently, I think.
				// IE considers setting the box's selection to be blurring the
				// the ce, while Chrome doesn't.
				
				box.selectionStart = Math.min( selectionStart, selectionEnd );
				box.selectionEnd = Math.max( selectionStart, selectionEnd );
				box.selectionDirection = isBackward ? 
					"backward" : 
					"forward";
				
			} else {
				// Contenteditable. Not tested at all.
				var originalRange = range.cloneRange(); // Unsupported by IE8
				// Use ranges and such for contenteditables.
				box.focus();
				range.setStart( box, selectionStart );
				range.setEnd( box, selectionEnd );
				selection.addRange( range );
				// ----
				selection.removeAllRanges();
				selection.addRange( originalRange );
				range.detach();
				originalRange.detach();
			}
			
			if ( reselect ) {
				caret.selectRange( range, isBackward );
			}
			// console.log( "end textBoxSync" );
		},
		/**
		 * Synchronize the caret/selection position and/or active Sign/NST in
		 * the SignArea to the textbox's caret position.
		 */
		reverseTextBoxSync: function ( SA, selectionStart, selectionEnd, selectionDirection ) {
			SA.updateTextboxContent();
			
			var //selectionStart, selectionEnd, 
				range, 
				collapsed,
				signs = SA.signs,
				elem,
				offset;
			if ( SA.type === "INPUT" ) {
				range = caret.getRange();
				// TODO.
				// ...
			} else {
				//selectionStart = SA.textBox.selectionStart;
				//selectionEnd   = SA.textBox.selectionEnd;
			}
			
			// Need to do a binary search again, apparently. I should just
			// create a generic function in the utilities section.
			// This is a complete duplicate.
			/**
			 * Returns the lowest number for which compareFunc returns true.
			 * @param {Function} compareFunc A test that returns true if it is
			 *  an accepted value.
			 * @param {Number} min Lowest allowed value, inclusive.
			 * @param {Number} max Highest allowed value, inclusive.
			 */
			function binarySearch( compareFunc, min, max ) {
				if ( min >= max ) {
					return min;
				}
				var mid = Math.floor( ( min + max ) / 2 );
				if ( compareFunc( mid ) === true ) {
					return binarySearch( compareFunc, min, mid );
				} else {
					return binarySearch( compareFunc, mid + 1, max );
				}
			}
			
			var q = binarySearch( function ( test ) {
				// Remember, binarySearch returns lowest true result.
				// Too hight=true, too low=false. 
				// false results in checking higher, true checks lower.
				
				// Find the earliest sign which doesn't start later than the
				// selection starts.
				// (The above description may no longer be correct.)
				
				// Um, this might actually be working fine, just the caret is
				// mispositioned before this is called.
				if ( test === 999 ) {
					return true;
					//return signs[ 0 ].toString().length < selectionStart;
					//return signs[0].toString().length > selectionStart ;
				} else {
					var curText = ( signs[ test + 1 ] || "" ).toString();
					// offset equals the number of characters occuring before
					// signs[ test ] start.
					var offset = fswParser.processMixedText(
							signs.slice( 0, test + 1 ).join( " " ) + " " +
							curText
						).length - curText.length;
					console.log( 12, "test=", test, offset, selectionStart );
					return offset >= selectionStart;
				}
			}, 0, signs.length );
			
			console.log( "%c reverseTextBoxSync: ", "color:purple;", "selectionStart =  ", selectionStart, "q=", q );
			
			//console.log( "%c MOVING CARET TO THIS SIGN: ", "color:orange;", signs[ q ], q, selectionStart, selectionEnd );
			if ( q >= signs.length ) {
				console.log( "Whaa?", q, signs.length );
				return;
			}
			var sign = signs[ q ];
			// Found the sign, focus it.
			sign.focus();
			
			offset = ( 
				q === 0 ? 
					0 :
					fswParser.processMixedText( 
						signs.slice( 0, q ).join( " " ) + " " + sign.toString()
					).length - sign.toString().length
			);
			offset = sign.textOffset();
			elem = sign.element;
			//caret.selectElement( elem );
			range = caret.getRange(); // unnecessary?
			console.log( "%c reverseTextBoxSync: ", "color:purple;", 
				"selectionStart =  ", selectionStart, 
				"q=", q, 
				"offset=", offset
				
			);
			
			if ( offset > selectionStart ) {
				console.error( "offset > selectionStart", offset, selectionStart, q );
			}
			
			try {
				if ( !sign.isSign ) {
					
					var childNodes = elem.childNodes, l = 0, curL;
					for ( var i = 0; ( elem = childNodes[ i ] ); i++) {
						console.log( 3333, elem );
						curL = elem.nodeType === 1 ?
							1 :
							elem.nodeValue.length;
						if ( l + curL > selectionStart - offset ) {
							// And we're done.
							try {
							range.setStart( elem, selectionStart - offset - l );
							range.setEnd(   elem, selectionEnd   - offset - l );
							caret.selectRange( range );
							}catch(e){
								console.error( "setStart/end broke", e, selectionStart, offset, l );
							}
							break;
						} else {
							l += curL;
						}
					}
					console.log( "POSITIONED TEXTBOX CARET", elem, selectionStart, offset, l );
				}
			} catch ( e ) {
				console.error( 741, e, range );
			}
			
			// signs[ q ].element.style.backgroundColor = "#00FF00";
			
		},
		/**
		 * Locate the caret, and if it's in a sign that's not the active one, 
		 * switch to that sign. If it's outside all the signs, switch to the the
		 * sign that's closest to the caret.
		 */
		check: function () {
			// Okay, rewriting this so that we have it run the first part, then
			// we do a sync based on it, and then set the caret. Save a trip.
			// Wait, maybe not. The caret's position isn't always so simple, and
			// 
			
			// TODO, maybe: Return a value if successful? 
			
			if ( !window.getSelection ) {
				// Old IE. TODO.
				return;
			}
			
			// TODO: Find some way to suppress the whole blur/focus SignArea 
			// thing.
			var selection = window.getSelection(),
				focusNode = selection.focusNode,
				focusOffset = selection.focusOffset,
				elem, i,
				signs = signArea && signArea.signs;
			// console.log( "Checking for caret change...");
			// console.log( "focusNode = ", focusNode );
			
			if ( !signArea ) {
				return;
			}
			
			if ( focusNode === activeSign.element ) {
				return;
			}
			
			if ( focusNode === signArea.element ) {
				// Currently focusing the SA itself. Use focusOffset to move
				// the activeSign and the caret to the right Sign.
				// Note: Both .focus and .changeSign move the caret, so long as
				// suppressCaret isn't set, but .changeSign won't work if the
				// sign is already active.
				if ( signs[ focusOffset ] === activeSign || focusOffset > signs.length ) {
					// Current sign, or error. Refocus currently active sign.
					activeSign.focus();
				} else if ( focusOffset === signs.length ) { 
					// Immediately after the last sign. Focus the last sign.
					if ( signs[ focusOffset - 1 ] === activeSign ) {
						// changeSign doesn't work on already active signs.
						activeSign.focus();
					} else {
						signArea.changeSign( focusOffset - 1 );
					}
				} else {
					signArea.changeSign( focusOffset );
				}
				return;
			}
			
			
			// console.log( "focusOffset = ", focusOffset );
			// Search for signListElem's descendants.
			for ( ; focusNode; focusNode = focusNode.parentNode ) {
				if ( focusNode === signListElem ) {
					break;
				}
				if ( /SWKB\-Sign|SWKB\-NonSign/.test( focusNode.className ) ) {
					if ( focusNode === activeSign.element ) {
						return;
					}
					for ( i = 0; i < signArea.signs.length; i++ ) {
						elem = signArea.signs[ i ].element;
						// console.log( "Checking ", elem );
						if ( elem === focusNode ) {
							console.log( "Found sign: ", signArea.signs[ i ] );
							console.log( "Changing sign." );
							signArea.changeSign( i, true );
							return;
						}
					}
					return;
				}
			}
			
			if ( !focusNode || focusNode !== signListElem ) {
				// console.log( "check: return false" );
				return false;
			}
			
			// The caret is outside all of the signs.
			// Binary search
			try {
				var range = selection.getRangeAt( 0 ),
					compareRange = document.createRange();
			} catch ( e ) {
				return;
			}
			
			// TODO: Real function name.
			function blee( min, max ) {
				if ( max === 0 )
					return 0;
				if ( min === max )
					return max;
				if ( min > max ) {
					console.log( "Wut" );
					return 0;
				}
				var mid = ( min + max ) >> 1;
				// console.log( "Comparing with Sign " + ( ( min + max ) >> 1 ) );
				compareRange.setEndAfter( signArea.signs[ ( min + max ) >> 1 ].element );
				switch ( range.compareBoundaryPoints( Range.END_TO_END, compareRange ) ) {
					case -1:
						return blee( min, mid - 1 );
					case 1:
						return blee( mid + 1, max );
					case 0:
						return mid;
				}
			}
			var targetIndex = blee( 0, signArea.signs.length - 1 );
			if ( targetIndex !== -1 ) {
				signArea.changeSign( targetIndex );
			}
			range.detach();
			
		},
		offset: function ( node, signElem, offset ) {
			if ( !signElem ) {
				// Find Sign element.
			}
			//console.log( "caretOffset(", node, ")" );
			// if ( node.className === "NonSignText" ) {
			var x = node === signElem;
			if ( node === signElem ) {
				node = signElem.childNodes[ offset ];
			}
			if ( !node || !node.previousSibling ) {
				return x ? 0 : offset;
			}
			for ( var l = 0; ( node = node.previousSibling ); ) {
				// <br>s add one, but sometimes don't have innerText anyways.
				l += node.nodeType === 1 ? 
					1 /* node[ innerText ].length */ : 
					node.nodeValue.length;
			}
			//console.log( "returning", l );
			return x ? l : l + offset;
		},
		/**
		 * Check whether a movement would move the cursor outside the NST.
		 * @param {number} change
		 * @return {boolean} Whether the caret would be outside the NST.
		 */
		locate: function ( change ) {
			if ( activeSign.isSign ) {
				return;
			}
			
			if ( caret.ieSelection ) {
				return; // TODO
			}
			
			// TODO: Simplify.
			var selection = window.getSelection(),
				offset = selection.focusOffset,
				//forward = change === 1,
				//edgeElem = elem[ forward ? "lastChild" : "firstChild" ],
				newOffset = offset + ( change || 0 ), 
				focusNode = selection.focusNode,
				elem = activeSign.element;
			if ( change === -1 ) {
				// Going backward.
				if ( offset <= 0 && ( focusNode === elem.firstChild || focusNode === elem ) ) {
					return true;
				}
			} else {
				// Going forward.
				var textLength = focusNode.nodeType === 1 ? 
					//focusNode[ innerText ] : // Is this expensive?
					// I don't know why I was using the above line before. Are
					// focus props inconsistent?
					focusNode.childNodes.length - !!activeSign.caretHolder : 
					( focusNode.nodeValue || "" ).length;
				if ( offset >= textLength && ( focusNode === elem.lastChild || focusNode === elem ) ) {
					return true;
				}
			}
			return false;
		},
		/**
		 * Within a NonSignText, move the caret.
		 * @param {number} dir Direction that the caret moves, positive = forwards.
		 * @param {Event} event
		 * @param {string} unit Optional override of the "unit" arg for modify.
		 */
		move: function ( dir, event, unit ) {
			//return;
			// No browsers currently support proper editable vertical elements, so
			// the up-down arrows need to be re-implemented.
			
			var elem = activeSign.element,
				targetSign;
			
			if (  // If we're at the edge of a non-sign, focus on the sign past the edge.
				unit !== "line" && caret.locate( dir )
			) {
				targetSign = activeSign.index() + dir;
				if ( signArea.signs[ targetSign ] ) {
					signArea.changeSign( targetSign );
				}
				return;
			}
			
			
			if ( "getSelection" in window ) {
				var selection = window.getSelection();
				if ( selection.modify ) {
					selection.modify( 
						event.shiftKey ? "extend" : "move", 
						dir === 1 ? "forward" : "backward", 
						// Does Ctrl+Sideways = paragraph movement?
						unit || ( event.ctrlKey ? "word" : "character" )
					);
					caret.keepInView();
				} else {
					//return true;
					// IE support. TODO.
					// NOTE: Direction control (and bi-dir extensions) are flat-out
					// impossible in some/all IEs.
					// Scratch that. I'm going to have this remember what direction
					// the selection is, and pretend everything's ordinary while
					// duplicating normal behaviour.
					var newOffset = offset + dir, 
						focusNode = selection.focusNode,
						range = selection.getRangeAt( 0 ),
						// Note: selection.focusOffset is not always the same as
						// range.endOffset.
						offset = selection.focusOffset, 
						x = offset + dir,
						length = focusNode.nodeType === 1 ? 
							1 : 
							( focusNode.nodeValue || "" ).length;
					
					// NOTE: Normalize doesn't always work in IE, so there can
					// be adjacent text nodes.
					
					
					// attempt 2
					if ( focusNode.nodeType === 1 ) {
						// Currently focusing an element node
						var target = focusNode.childNodes[ offset - ( dir !== 1 ) ];
						if ( focusNode === signArea.element ) {
							//console.log( ',,,1', target );
							// Currently focusing the SignArea
							target = target[ dir !== 1 ? "firstChild" : "lastChild" ];
						}
						/*
						if ( focusNode === activeSign.caretHolder ) {
							target = focusNode;
						}
						*/
						//console.log( ',,,', target, focusNode );
						if ( target.nodeType === 3 ) {
							// Target is a text node
							var pos = dir === 1 ? 1 : target.length - 1;
							range.setStart( target, pos );
							range.setEnd( target, pos );
						} else {
							if ( dir === -1 ) {
								range.setStartBefore( target );
								range.setEndBefore( target );
							} else {
								range.setStartAfter( target );
								range.setEndAfter( target );
							}
						}
					} else {
						// Presumably a text node.
						var target = focusNode[ ( x < 0 ? "previous" : "next" ) + "Sibling" ],
							targetIsText = target && target.nodeType === 3;
						if ( x < 0 ) {
							if ( focusNode.previousSibling ) {
								if ( targetIsText ) {
									// This whole thing might not have been 
									// necessary. TODO: CHECK, before saving.
									range.setStart( target, 0 );
									range.setEnd( target, 0 );
								} else {
									range.setStartBefore( focusNode.previousSibling );
									range.setEndBefore( focusNode.previousSibling );
								}
							}
						} else if ( x > length ) {
							if ( focusNode.nextSibling ) {
								if ( targetIsText ) {
									var targetLength = target.nodeValue.length;
									range.setStart( target, targetLength );
									range.setEnd( target, targetLength );
								} else {
									range.setStartAfter( focusNode.nextSibling );
									range.setEndAfter( focusNode.nextSibling );
								}
							}
						} else {
							range.setEnd( focusNode, x );
							event.shiftKey && dir === 1 || range.setStart( focusNode, x );
						}
					}
					
					//var busy = caret.busy;
					caret.selectRange( range );
					//caret.busy = busy;
					//console.log( x, range.endOffset, "END MOVE" );
				}
			} else {
				// Old IE.
				// Not checked
				var selection = document.selection,
					range = selection.createRange();
				range.move( unit === "line" ? "line" : "character", dir );
				range.select();
			}
			// caret.focus(); // maybe
		},
		// Unused.
		focus: function () {
			var r = caret.getRange(), e;
			caret.busy = true;
			r.insertNode( e = document.createElement( "input" ) );
			e.focus();
			e.parentNode.removeChild( e );
			caret.selectRange( r );
		},
		// Move both somewhere
		/**
		 * Retrieve the width of the viewport.
		 */
		docWidth: ( function () {
			var confused, docElem = document.documentElement, cachedWidth;
			return function () {
				if ( confused === undefined ) {
					var coords = 
						docElem.getBoundingClientRect();
					confused = coords.right - coords.left !== document.documentElement.clientWidth;
				}
				return caret._docWidth || 
					( caret._docWidth = docElem[ confused ? "clientHeight" : "clientWidth" ] );
			}
		})(),
		_docWidth: undefined,
		/**
		 * Scroll the page to the right by the specified amount.
		 * @param {number} x
		 */
		scrollToRightBy: ( function () {
			var first, second;
			return function ( x ) {
				// This still needs some cleaning up.
				if ( first === undefined ) {
					// Remember, all of these have unreliable orientation.
					var sLeft = document.documentElement.scrollLeft || 
							document.body.scrollLeft,
						sTop = document.documentElement.scrollTop ||
							document.body.scrollTop;
					
					if ( sLeft + x !== sTop ) {
						scrollBy( 0, x );
					} else {
						// What to do?
						return;
					}
					
					var coords = document.body.getBoundingClientRect(),
						leftOfScreen = Math.round( -coords.left );
					if ( false /*coords.top === leftOfScreen*/ ) {
						// Ugh. Impossible to tell.
					} else if ( leftOfScreen === 
						( document.documentElement.scrollLeft || document.body.scrollLeft )
					) {
						first = 1; second = 0;
					} else if ( leftOfScreen === 
						( document.documentElement.scrollTop || document.body.scrollTop )
					) {
						scrollBy( x, -x );
						second = 1; first = 0;
					} else {
						// Results unclear. 
						// ??
					}
				} else {
					scrollBy( x * first, x * second );
				}
			}
		})(),
		/**
		 * Make sure that the caret or selected element is in view.
		 */
		keepInView: function () {
			// Remember: Everything's going to be mixed up in IE. Need to figure
			// out some way to fix this.
			var c, z, saRects, moved = 0;
			//c = r.getClientRects();
			//c = r.getBoundingClientRect();
			//r.extend( 1 );
			
			if ( activeSign.isSign ) {
				//c = activeSign.element.getClientRects()[ 0 ];
				c = activeSign.element.getBoundingClientRect();
			} else {
				// Find the caret. 
				// Maybe just extend r a bit earlier and use that...
				var r = caret.getRange();
				if ( getSelection().type === "Caret" ) {
					// For some reason, Chrome (and maybe other browsers?) can't
					// handle gCR on empty selections in vertical writing modes.
					// Some 0.3% of keepInView's time is taken up creating the
					// text node. Should it be cached?
					r.insertNode( z = document.createTextNode( "\u200b" ) );
					r.selectNode( z );
					// c has 0 height, so it has no real bounding box. As a 
					// result, only regular gCR will work. gBCR returns all 0s.
					// This is a Webkit bug, I think.
					c = r.getClientRects()[ 0 ];
					//c = r.getBoundingClientRect();
					
					// Use removeChild instead of this? Need to do some tests.
					r.deleteContents();
				} else {
					// Note that gBCR is faster than gCR.
					c = r.getBoundingClientRect();
				}
				r.detach();
			}
			
			saRects = signArea.getCoords();
			
			// If the caret is outside the visible portion of the text box, then
			// scroll the text box to the right location.
			if ( c.right > saRects.right ) {
				signArea.container.scrollLeft += ( moved = c.right - saRects.right );
			} else if ( c.left < saRects.left ) {
				signArea.container.scrollLeft += ( moved = c.left - saRects.left );
			}
			
			// If the caret is outside the visible portion of the window, scroll
			// the window.
			var docWidth = caret.docWidth();
			if ( c.left - moved < 0 ) {
				caret.scrollToRightBy( c.left - moved );
			} else if ( c.right - moved > docWidth ) {
				caret.scrollToRightBy( c.right - docWidth - moved );
			}
		
		},
		// Unused
		escapeSign: function () {
			// Well, this didn't work.
			if ( config.ce ) {
				var range = caret.getRange(), 
					saElem = signArea.element,
					index = activeSign.index();
				range.setStart( saElem, index );
				range.setEnd(   saElem, index );
				caret.selectRange( range );
				range.detach();
				caret.needsCheck = true;
			}
		},
		// These two functions have some redundancies. TODO: Fix.
		handleClipboard: function ( e, data ) {
			// This is copied straight from ACE. Hopefully not an IP 
			// infringement or anything...
			var clipboardData = e.clipboardData || window.clipboardData;
			if ( !clipboardData /* || BROKEN_SETDATA */ ) {
				return;
			}
			var IE = (navigator.appName == "Microsoft Internet Explorer" || navigator.appName.indexOf("MSAppHost") >= 0)
			    ? parseFloat((navigator.userAgent.match(/(?:MSIE |Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1])
			    : parseFloat((navigator.userAgent.match(/(?:Trident\/[0-9]+[\.0-9]+;.*rv:)([0-9]+[\.0-9]+)/)||[])[1])
			var mime = IE ? "Text" : "text/plain";
			
			// TEMPORARY
			if ( IE ) {
				return false;
			}
			
			if ( data ) {
				return clipboardData.setData( mime, data ) !== false;
			} else {
				return clipboardData.getData( mime );
			}
		},
		copyFSW: function ( e ) {
			// TODO (Longer-term): Only copy highlighted/selected parts.
			// If there's a selection, copy the contents (inclusive of Signs).
			// If there isn't a selection, and a Sign is active, copy that.
			
			var toBeCopied,
				copyPasteBox = caret.copyPasteBox;
			
			// Don't want to copy a off-center sign. Repair things first.
			activeSign.centerSign && activeSign.centerSign();
			
			if ( config.copyAll ) {
				toBeCopied = signArea.toString();
			} else {
				// TODO: Expand this. A lot.
				if ( activeSign.isSign ) {
					toBeCopied = activeSign.toString();
				} else {
					return true;
				}
			}
			
			if ( caret.handleClipboard( e, toBeCopied ) ) {
				e.preventDefault && e.preventDefault();
				return false;
			} else {
				// For the moment, this will happen somewhat frequently, since
				// copyFSW will still be fired on Ctrl-C, even with an empty
				// range.
				// console.log( "Using old copy method" );
			}
			
			// Make sure that we don't lose the old caret/selection.
			if ( config.ce ) {
				caret.busy = true;
				var range = caret.getRange();
			}
			// Move the selection to a textarea element that has the value set
			// to the text to be copied.
			document.body.appendChild( copyPasteBox );
			copyPasteBox.value = toBeCopied;
			copyPasteBox.select();
			// keypress isn't always supported when ctrl is held, so use a simple
			// setTimeout for this.
			// Actually, I should probably use "onchange" or something for this.
			// Oncopy isn't supported by Opera.
			setTimeout( function () {
				if ( copyPasteBox.parentNode ) {
					document.body.removeChild( copyPasteBox );
					// Return the caret back to the sign.
					config.ce && caret.selectRange( range );
				}
			}, 16 );
			caret.busy = false;
		},
		pasteFSW: function ( e ) {
			// Okay, this has become complicated.
			// Pasting while NST is active means that the NST should be split, 
			// using the caret as the divider. Unfortunately, the caret is
			// removed during the paste process. Cacheing it is difficult.
			// InterpretFSW is not build specifically for pasting, and it can't
			// focus stuff by itself.
			// However, it does merge NSTs by itself.
			// Pasting plainText into an NST needs to work right.
			// Also pasting plainText right before NST, and after.
			// The caret needs to be located correctly in these situations.
			// That's inside the NST, after where the pasted and already merged
			// content is. iFSW handles both splits and merges by itself, but
			// not in such a way that makes it easy to focus things right.
			
			var val = caret.handleClipboard( e ),
				range,
				lastCopied,
				copyPasteBox = caret.copyPasteBox;
			
			if ( typeof val === "string" ) {
				e.preventDefault && e.preventDefault();
				lastCopied = interpretFSW( val );
				if ( lastCopied && lastCopied !== activeSign ) {
					activeSign.blur();
					lastCopied.focus();
				}
				return false;
			} else {
				// console.log( "Using old copy method" );
			}
			
			caret.busy = true;
			if ( config.ce && !activeSign.isSign ) {
				range = caret.getRange();
			}
			document.body.appendChild( copyPasteBox );
			copyPasteBox.value = "";
			copyPasteBox.select();
			// keypress isn't always supported when ctrl is held, so use a simple
			// setTimeout for this.
			setTimeout( function () {
				if ( copyPasteBox.parentNode ) {
					range && activeSign.split( range );
					lastCopied = interpretFSW( copyPasteBox.value );
					if ( lastCopied ) {
						activeSign.blur();
						//console.log( 999, lastCopied );
						lastCopied.focus();
					}
					document.body.removeChild( copyPasteBox );
				}
			}, 16 );
			caret.busy = false;
		},
		togglePlaintext: function () {
			if ( config.ce === false ) {
				return;
			}
			
			var oldSign = activeSign, 
				newSign,
				index = activeSign.index();
			
			if ( !oldSign.isSign ) {
				/*
				var selection = window.getSelection(),
					range  = selection.getRangeAt(0),
					newSign;
				*/
				
				oldSign.split();
				newSign = signArea.addSign( index + 1 );
				oldSign.blur();
				newSign.focus();
			} else {
				
				// If adjacent to NST, focus that (at the end if NST is before
				// current sign).
				// If surrounded by NSTs, focus earlier one, then merge.
				var prevSign = signArea.signs[ index - 1 ],
					followingSign = signArea.signs[ index + 1 ];
				activeSign.centerSign();
				activeSign.blur();
				
				// I think there's some duplication with deleteSign here.
				if ( prevSign && !prevSign.isSign && activeSign.isEmpty() ) {
					// Situation: NST [Empty Sign]. Focus the NST at the end and 
					// delete the empty Sign later. 
					prevSign.focus( true );
					caret.selectElement( prevSign.element, true, true );
					if ( followingSign && !followingSign.isSign ) {
						// Situation: NST [Empty Sign] NST. Merge NSTs.
						prevSign.mergeWith( followingSign );
					}
				} else {
					if ( followingSign && !followingSign.isSign ) {
						// Situation: [Sign] NST. Focus NST.
						followingSign.focus();
					} else {
						// Situation: Anything else. Create new NST after Sign.
						newSign = signArea.addNonSignText( index + 1 );
						newSign.focus();
					}
				}
				console.log( 121, document.activeElement );
			}
			
			if ( oldSign.isEmpty() ) {
				signArea.removeSign( oldSign );
			}
			
			return false; // Don't add the ` to the box.
		},
		// Selection stuff. Unclear how to handle these...
		selectionStart: undefined,
		selectionEnd:   undefined,
		selectionDir:   undefined,
		copyPasteBox: ( function () {
			copyPasteBox = document.createElement( "textarea" ),
			copyPasteBox.className = "SWKB-copyPasteBox";
			return copyPasteBox;
		} )()
		// Need some functions. Selections can be expanded, eliminated, have 
		// their contents deleted or copied, (how am I going to handle that,
		// by the way? Some people might copy by rightclick>copy...) Argh.
	};
	
	// ** STORED PREFERENCES **
	prefs = {
		changed: false,
		enabled: !!( window.JSON && window.localStorage ),
		prefs: undefined,
		set : function ( name, value ) {
			if ( prefs.prefs[ name ] !== value ) {
				prefs.prefs[ name ] = value;
				prefs.changed = true;
			}
		},
		get : function ( name ) {
			return prefs.prefs[ name ];
		},
		init: function () {
			if ( prefs.enabled ) {
				var p = localStorage[ "SWKB-prefs" ];
				prefs.prefs = p ? JSON.parse( p ) : {};
				addEventListener( window, "unload", function () {
					if ( prefs.changed ) {
						// Save the prefs in localStorage before leaving.
						localStorage[ "SWKB-prefs" ] = 
							JSON.stringify( prefs.prefs );
					}
				} );
			} else {
				prefs.prefs = {};
			}
		}
	};
	
	// ** TEXT BOX HANDLING **
	
	// Should there be some way to re-trigger this? If a new textbox gets added,
	// there needs to be some way to identify it.
	// I'm thinking window.onfocus, if that event bubbles. Not sure if it does.
	// Also, maybe move this into init.
	function setupTextboxListeners() {
	
		/**
		 * Make a textbox use the SignWriting keyboard.
		 */
		// TODO: Better name.
		// This function should probably be available in the SWKB global.
		// Need to fiddle with this somehow. I want placeholder properties to be
		// acted upon immediately where appropriate, and maybe also for boxes to
		// look like proper sign-editable fields right away. Also, if it has 
		// prefilled content, definitely run. Maybe merge with SignArea function.
		function SWKBify( node ) {
			var nodeSignArea;
			// This should really start setting things up before being focused, at
			// least in certain cases. Text boxes with content need to appear 
			// correctly in advance of editing.
			
			if ( document.activeElement === node ) {
				nodeSignArea = new SignArea( node );
			}
			
			// I think there's a class that's supposed to suppress things like 
			// special input methods... Might need to add a check for it. 
			// Need to look into that.
			// The class is "noime", but I don't know if it's used at all 
			// outside ULS and such.
			addEventListener( node, "focus", function ( e ) {
				/*
				if ( delegateEvent( e ) === false ) {
					return false;
				}
				*/
				
				if ( disabled || e.fakeEventPleaseIgnore ) {
					return true;
				}
				console.log( "FOCUSED TEXTBOX. caret.busy=", caret.busy );
				if ( !nodeSignArea ) {
					nodeSignArea = true; // TODO: Figure out whether this is necessary.
					nodeSignArea = new SignArea( this );
				} else {
					//return;//123456
					if ( !caret.busy && config.ce ) {
						// Problem: This timer can sometimes run out before the
						// oninput-simulating timer runs out, causing hTBC to
						// be triggered erroneously. 
						setTimeout( function () {
							if ( node.value !== nodeSignArea.toString() && !caret.busy ) {
								//console.log( 2343, nodeSignArea.textBox.selectionStart );
								nodeSignArea.handleTextboxChange();
								//console.log( 2344, nodeSignArea.textBox.selectionStart );
								// I don't actually know if this is necessary.
								// Probably not, since it seems to be called by
								// hTBC itself.
								// Just in case, I guess.
								//caret.reverseTextBoxSync( nodeSignArea );
							}
						}, 1 );
					}
				}
				caret.busy || nodeSignArea.focus();
			}, true );
			addEventListener( node, "select", function ( e ) {
				// Is this actually necessary? Shouldn't focus take care of all the
				// necessary change reactions?
				return;
				console.log( "select called. caret.busy=" + caret.busy );
				if ( !caret.busy && config.ce ) {
					caret.busy = true;
					console.log( "select called. selectionStart="+nodeSignArea.textBox.selectionStart );
					if ( node.value !== nodeSignArea.toString() ) {
						nodeSignArea.handleTextboxChange();
					}
					console.log( "select called. selectionStart="+nodeSignArea.textBox.selectionStart );
					// Temporarily remove
					//caret.reverseTextBoxSync( nodeSignArea );
					console.log( "select called. selectionStart="+nodeSignArea.textBox.selectionStart );
					caret.busy = false;
				}
			});
			// Note: oninput does not activate on JS inputs.
		}
		
		// TODO
		var textAreas = document.getElementsByTagName( "textarea" ),
			inputs = document.getElementsByTagName( "input" ), 
			i,
			done = [];
		for ( i = 0; i < textAreas.length; i++ ) {
			SWKBify( textAreas[ i ] );
			done.push( textAreas[ i ] );
		}
		for ( i = 0; i < inputs.length; i++ ) {
			if ( inputs[ i ].type === "text" || inputs[ i ].type === "search" ) {
				SWKBify( inputs[ i ] );
				done.push( inputs[ i ] );
			}
		}
		
		// 
		addEventListener( 
			document.body, 
			"focus", 
			function ( e ) { 
				if ( caret.busy ) {
					return;
				}
				var target = e.target, nodeName = target.nodeName;
				if ( 
					( nodeName === "TEXTAREA" || 
					( nodeName === "INPUT" && 
						( target.type === "text" || target.type === "search" )
					) ) &&
					done.indexOf( target ) === -1 &&
					target !== caret.copyPasteBox &&
					//( " " + target.className + " " ).indexOf( " noime " ) === -1
					// Ignore text boxes with the "noime" class.
					!/(^|[\t\r\n\f ])noime($|[\t\r\n\f ])/.test( target.className )
				) {
					done.push( target );
					//new SignArea( target );
					// PROBLEM: This is causing an extra SA to be built.
					// Fixed by disabling above. Does that cause any problems?
					SWKBify( target );
				}
			},
			true
		);
	}
	
	// ** UTILITIES **
	
	function cloneArray( source ) {
		for ( var i = 0, x = []; i < source.length; i++ ) {
			x[ i ] = source[ i ];
		}
		return x;
	}
	
	function simpleExtend( source, target ) {
		if ( source ) {
			for ( var i in source ) {
				target[ i ] = source[ i ];
			}
		}
		return target;
	}
	
	// This needs a better name. "Delegating" events means something specific,
	// and it's not this.
	function delegateEvent( evt, target ) {
		if ( evt.fakeEventPleaseIgnore ) {
			return false;
		} else {
			target = target || ( signArea && signArea.textBox );
			if ( target ) {
				var event = document.createEvent('Event');
				event.initEvent( evt.type, true, true );
				for ( var i in evt ) {
					if ( evt.hasOwnProperty( i ) ) {
						event[ i ] = evt[ i ];
					}
				}
				// There needs to be a better way to do this.
				event.fakeEventPleaseIgnore = true;
				
				// "Delegate" (not in the classical sense) event to the textbox.
				target.dispatchEvent( event );
			}
			return true;
		}
	}
	
	// Work in progress. Currently contains a lot of duplication of existing
	// stuff, which needs to be moved over.
	var fswParser = ( function () {
		
		// ** REGULAR EXPRESSIONS **
		var simpleSymbolPattern = 
				// Symbol type
				"S[123][0-9a-f]{2}[0-5][0-9a-f]" + 
				// Symbol position
				"[0-9]{3}x[0-9]{3}",
			simpleSymbolReg = new RegExp( simpleSymbolPattern, "g" ),
			// With captures
			symbolReg = /^(S[123][0-9a-f]{2}[0-5][0-9a-f])([0-9]{3})x([0-9]{3})$/,
			symbolTypeReg = /S[123][0-9a-f]{2}[0-5][0-9a-f]/g,
			spellingSequencePattern = "A(?:S[123][0-9a-f]{2}[0-5][0-9a-f])+",
			spellingSequenceReg = new RegExp( "^" + spellingSequencePattern ),
			// Basic Sign, no captures.
			signPattern = 
				// Spelling sequence
				"(?:" + spellingSequencePattern + ")?" + 
				// Lane and sign size
				"(?:[BLMR][0-9]{3}x[0-9]{3}" + 
				// Symbols
				"(?:" + simpleSymbolPattern + ")*" + 
				// Weird punctuation stuff
				"|S38[7-9ab][0-5][0-9a-f][0-9]{3}x[0-9]{3})",
			// Under ordinary conditions, there should be a space between each Sign
			// and NonSignText. However, we still want things like slashes, 
			// namespaces (":"), links, and tags to work without having extra spaces 
			// breaking things. Also, there shouldn't be a space after a linebreak
			// unless specified, as that will generate a pre tag. So, if we have
			// one of these characters before/after a Sign, one space gets removed.
			// In the other direction, if we have FSW, don't start clearing out
			// extra spaces if we didn't expect them. Leave them in as extra so we
			// don't get "dirty diffs". There is frequently legitimate use for 
			// having these spaces. For example, a wiki external link could have
			// the URL end in a slash, but we still need the space to have it work.
			// Characters to consider adding: "#" (for both anchors and numbers), 
			// "*" (perhaps for bullet points), "@", quote marks (HTML attributes),
			// ...
			dontTrimAfterPattern  = "[\\[\\=\\n\\|\\/\\:\\>]",
			dontTrimBeforePattern = "[\\]\\=\\n\\|\\/\\:\\<]",
			// TODO: Remove some unnecessary brackets from the regexp.
			// Okay, this is all borked. Need to redo.
			// Unused.
			parseFSWReg = new RegExp( 
				"([\\s\\S]*?" + // Base NonSignText stuff
				// Trim whitespace, except if preceded by certain characters.
				"(?:" + dontTrimAfterPattern + "\ +?(?! ))?)\ ?" + 
				// Get the Sign if there is one, or just go to the end.
				"($|" + signPattern + ")" + 
				// Trim whitespace, except if followed by certain characters.
				"(?:\ (?! *" + dontTrimBeforePattern + "))?", "g" ),
			// TODO: Write some tests for this.
			produceFSWReg = new RegExp( 
				"(?:(" + dontTrimAfterPattern + " *?) )?" +
				"(" + signPattern + ")" + 
				// "(?:\ ([\\]\\=\\n\\|\\/\\:]))?",
				"(?: (?= *?" + dontTrimBeforePattern + "))?",
				"g"
			),
			// replacement for parse
			// For all I know, this might be unbearably slow. Need to check.
			signReg = new RegExp( signPattern, "g" ),
			// Unused. Remove?
			fixWhitespace = new RegExp( 
				"^(?: (?= *" + dontTrimBeforePattern + "))?" +
				"([\\s\\S]+" + 
				"(?:" + dontTrimAfterPattern + "? *)) ?$",
				"g"
			),
			// Do either of these need the global flag? Probably not.
			fixPreWhitespace = new RegExp( 
				"^ (?! *" + dontTrimBeforePattern + ")",
				"g"
			),
			fixPostWhitespace = new RegExp( 
				// Okay, this can be improved.
				"((?!" + dontTrimAfterPattern + ")(?:^|[^ ]) *) $",
				"g"
			);
		
		return {
			// Consider renaming if there's also going to be a function for
			// getting actual SignSymbols from text.
			/**
			 * @param {string} text FSW to parse.
			 * @param {function} callback
			 * @return {number} Number of symbols found.
			 */
			getAllSymbols: function ( text, callback ) {
				for ( var result, i = 0; ( result = simpleSymbolReg.exec( text ) ); i++ ) {
					callback && callback( result[ 0 ] );
				}
				// Return the number of symbols.
				return i;
			},
			// Unused. (Can't work for interpretFSW bc it needs ex.index.)
			getAllSigns: function ( text, callback ) {
				for ( var result; ( result = signReg.exec( text ) ); ) {
					callback && callback( result[ 0 ] );
				}
			},
			getSymbolsFromSequence: function ( sequence, callback ) {
				for ( var result; ( result = symbolTypeReg.exec( sequence ) ); ) {
					callback( result[ 0 ] );
				}
			},
			/**
			 * Process mixed FSW and plaintext, removing extra whitespace 
			 * generated by the SignArea's join( ' ' ) where appropriate. 
			 * (For converting content objects to FSW.)
			 * @param {string} text Mixed FSW and plaintext to be processed.
			 * @return {string}
			 */
			processMixedText: function processMixedText( text ) {
				return text.replace( produceFSWReg, "$1$2" );
			},
			/**
			 * Remove extra whitespace from NonSignText strings, for converting 
			 * from FSW to NonSignText content objects.
			 */
			clearExtraWhitespace: function ( text ) {
				return text
					.replace( fixPreWhitespace, '' )
					.replace( fixPostWhitespace, "$1" );
			},
			regexes: {
				signReg: signReg,
				fixPreWhitespace: fixPreWhitespace,
				fixPostWhitespace: fixPostWhitespace
			}
		};
	} )();
	
	/**
	 * Takes a string containing FSW and/or plain text and adds the content to
	 * the SignArea as Sign and NonSignText objects.
	 * @param {string} fullText
	 * @param {number} [startPosition] Where to insert the new 
	 *  Signs and NonSignTexts. Default position is after the activeSign.
	 * @return {Sign|NonSignText} Last Sign added to the SignArea
	 */
	function interpretFSW( fullText, startPosition ) {
		// Maybe this should be moved into SignArea.prototype?
		
		// Incomplete. Much of this will probably be rewritten so that it's not
		// so slow.
		// Most recent test: 393ms on AS1dc20S1c... Still too slow.
		// More recent test: 146ms. (After parse-only-on-focus was implemented.)
		if ( activeSign.isSign ) {
			activeSign.centerSign();
		}
		
		if ( startPosition === undefined ) {
			startPosition = activeSign.index() + 1;
		}
		
		//console.log( "interpretFSW", fullText, startPosition );
		
		// TODO: Blur the original activeSign, or refocus it afterwards. Maybe.
		var newSign,
			signs = signArea.signs,
			firstSign = activeSign,
			index = startPosition,
			signReg = fswParser.regexes.signReg,
			fixPreWhitespace = fswParser.regexes.fixPreWhitespace;
		
		if ( signs[ startPosition - 1 ] && !signs[ startPosition - 1 ].isSign ) {
			signs[ startPosition - 1 ].split();
		}
		
		var pInd = 0;
		
		for( var text, extraText; ( text = signReg.exec( fullText ) ); ) {
			//console.log( "iFSW", text );
			// First, deal with the extra text between signs.
			//extraText = text[ 1 ];
			
			extraText = fullText.substring( pInd, text.index );
			pInd = text[ 0 ].length + text.index;
			if ( extraText && extraText !== " " && extraText.length > 0 /* && config.ce */ ) {
				
				// extraText is all text between the signs. Remove any extra 
				// whitespace, except before/after certain characters.
				extraText = fswParser.clearExtraWhitespace( extraText );
				// oops. this was backward.
				// " ]" > " ]", " a" > "a"
				// TODO
				
				// Add a normal NST containing the extraText.
				newSign = signArea.addNonSignText( index++, extraText );
			}
			
			// Next, create an actual Sign, if there is one.
			newSign = signArea.addSign( index++, text[ 0 ] );
		}
		
		// Add remaining plainText, if there is any
		if ( pInd < fullText.length ) {
			newSign = signArea.addNonSignText( 
				index++, 
				fullText.substr( pInd ).replace( fixPreWhitespace, '' )
			)
		}
		
		// I don't like this.
		// TODO: Fix things so that the caret moves after added stuff while
		// pasting content.
		if ( 
			signs[ index - 1 ] && !signs[ index - 1 ].isSign && 
			signs[ index ] && !signs[ index ].isSign
		) {
			signs[ index - 1 ].mergeWith( signs[ index ] );
		}
		
		if ( 
			signs[ startPosition ] && !signs[ startPosition ].isSign && 
			signs[ startPosition - 1 ] && !signs[ startPosition - 1 ].isSign
		) {
			signs[ startPosition - 1 ].mergeWith( signs[ startPosition ] );
		}
		
		// If the Sign that was active before this all started is empty, get rid
		// of it. NOTE: This can cause NST-followed-by-NST issues. TODO: Fix.
		if ( firstSign && firstSign.isEmpty() ) {
			signs[ firstSign.index() + 1 ].focus();
			signArea.removeSign( firstSign );
			// activeSign.focus();
		}
		
		return newSign; // This is only returning the most recent sign. ?
	}
	
	function startWorker() {
		// Not finished.
		// Set up some web workers to offload some heavy stuff, such as canvas
		// measurements of symbols that are not needed immediately.
		// TODO.
		if ( window.Blob && window.URL && window.Worker ) {
			try {
				// Hm. WWs can only run isolated, and apparently can't be 
				// given functions.
				var blob = new Blob( [
					"onmessage = function ( q ) { postMessage( [ 'b' ] ); };"
				] );
				var blobURL = window.URL.createObjectURL( blob );
				var worker = new Worker( blobURL );
				worker.onmessage = function ( d ) {
					console.log( d );
				};
				
				//worker.postMessage( foo );
			} catch ( e ) {}
		}
	}
	
	var fontHandler = ( function () {
		// Temporarily copied from sw10 by Stephen Slevinski. To be removed, as
		// soon as the sw10 library is directly available.
		
		// To consider: Using toDataURL for the keyboard key elements, to have
		// the symbols as simple background images.
		var canvas,
			canvas2,
			context,
			bound,
			initialized = false,
			widthOverride = {
				S1710d : '20',
				S1711d : '20',
				S1712d : '20',
				S17311 : '20',
				S17321 : '20',
				S17733 : '20',
				S1773f : '20',
				S17743 : '20',
				S1774f : '20',
				S17753 : '20',
				S1775f : '20',
				S16d33 : '20',
				S1713d : '20',
				S1714d : '20',
				S17301 : '20',
				S17329 : '20',
				S1715d : '20',
				S24c15 : '22',
				S24c30 : '22',
				S2903b : '23',
				S1d203 : '25',
				S1d233 : '25',
				S24c15 : '28',
				S2e629 : '29',
				S16541 : '30',
				S23425 : '30',
				S2d316 : '40',
				S2541a : '50'
			}, 
			heightOverride = {
				S1732f : '20',
				S17731 : '20',
				S17741 : '20',
				S17751 : '20',
				S1412c : '21',
				S2a903 : '31',
				S2b002 : '36'
			}, 
			allOverride = {
				S10008 : "15x30", 
				S10009 : "21x30", 
				S1000a : "30x15",
				S1000b : "30x21",
				S1000c : "15x30",
				S1000d : "21x30"
			};
		
		function size( key ) {
			if ( allOverride[ key ] ) {
				// BUG: Alloverride symbols don't display. Might be caused by
				// not dealing with .loaded below.
				//return allOverride[ key ];
				
				// TEMPORARY
				heightOverride[ key ] = allOverride[ key ].split( "x" )[ 1 ];
				widthOverride[ key ] = allOverride[ key ].split( "x" )[ 0 ];
			}
			context.clearRect( 0, 0, bound, bound );
			var xb = Date.now();
			var text = fontHandler.code( key ), width, height;
			context.fillText( text, 0, 0 );
			//console.log( "TIME:", Date.now() - xb );
			var i, imgData = context.getImageData( 0, 0, bound, bound ).data;
			
			/*
			for ( var w = bound - 1; w >= 0; w-- ) {
				// if ( Array.prototype.join.call( context.getImageData( w - 1, 0, 1, bound ).data, "" ) !== blankRowOrColumn ) {
				//if ( Math.max.apply( Math, context.getImageData( w, 0, 1, bound ).data ) !== 0 ) {
				if ( Math.max.apply( Math, imgData.subarray( w * 4, w * 4 + bound * 4 ) ) !== 0 ) {
					break;
				}
			}
			var width = w;
			for ( var h = bound - 1; h >= 0; h-- ) {
				if ( Math.max.apply( Math, context.getImageData( 0, h, 1, w ).data ) !== 0 ) {
					break;
				}
			}
			//console.log( "TIME2:", Date.now() - xb );
			var height = h + 1;
			width = '' + Math.ceil( width / 2 );
			height = '' + Math.ceil( height / 2 );
			*/
			
			var bound4 = bound * 4;
			if ( context.measureText ) {
				width = context.measureText( text ).width;
			} else {
				wloop:
					for ( var w = bound - 1, w4; w >= 0; w-- ) {
						w4 = w * 4;
						for ( var h = 0; h < bound; h++ ) {
							i = w4 + ( h * bound4 ) + 3;
							if ( imgData[ i ] !== 0 ) {
								break wloop;
							}
						}
					}
				width = w;
			}
			if ( width === 0 ) {
				return '';
			}
			hloop:
				for ( var h = bound - 1, h4; h >= 0; h-- ) {
					h4 = h * bound4;
					for ( var w = 0; w < width; w++ ) {
						i = w * 4 + h4 + 3;
						if ( imgData[ i ] !== 0 ) {
							break hloop;
						}
					}
				}
				
			//console.log( "TIME2:", Date.now() - xb );
			height = h + 1;
			if ( height === 0 ) {
				return '';
			}
			width = widthOverride[ key ] || ( '' + Math.ceil( width / 2 ) );
			height = heightOverride[ key ] || ( '' + Math.ceil( height / 2 ) );
			
			var size = width + 'x' + height;
			if ( size === '0x0' ) {
				return '';
			}
			fontHandler.loaded = true;
			return size;
		
		}
		
		function code( key ) {
			var code = 0x100000 + 
				(
					( parseInt( key.slice( 1, 4 ), 16 ) - 256 ) * 96
				) + 
				(
					( parseInt( key.slice( 4, 5 ), 16 ) ) * 16 
				) +
				parseInt( key.slice( 5, 6 ), 16 ) + 1;
			return String.fromCharCode( 
					0xD800 + ( ( code - 0x10000 ) >> 10 ), 
					0xDC00 + ( ( code - 0x10000 ) & 0x3FF )
				);
		}
		
		function getImage( fsw, color ) {
			canvas2 = canvas2 || document.createElement( "canvas" );
			if ( heightCache[ fsw ] ) {
				canvas2.height = heightCache[ fsw ];
				canvas2.width = widthCache[ fsw ];
			} else {
				var x = fontHandler.size( fsw ).split( "x" );
				canvas2.height = heightCache[ fsw ] = +x[ 1 ];
				canvas2.width = widthCache[ fsw ] = +x[ 0 ];
			}
			var context = canvas2.getContext( "2d" ),
				text = code( fsw );
			context.font = "30px 'SignWriting 2010'";
			context.fillStyle = color ? "#" + color : '#000000';
			context.fillText( text, 0, 0 );
			context.font = "30px 'SignWriting 2010 Filling'";
			context.fillStyle = '#FFFFFF';
			context.fillText( text, 0, 0 );
			return canvas2.toDataURL();
		}
		
		/**
		 * Listen for when the font finishes loading.
		 */
		function loadListener() {
			// Thanks to smnh for figuring out how to detect font loading.
			// http://smnh.me/web-font-loading-detection-without-timers/
			
			// console.log( "loadListener starting" );
			
			// First, we create four nested divs, listed in order here.
			var container = document.createElement( "div" ),
			    outer = container.appendChild( document.createElement( "div" ) ),
				middle = outer.appendChild( document.createElement( "div" ) ),
				inner = middle.appendChild( document.createElement( "div" ) );
			
			// container > outer > middle > inner
			
			// Next, styling. We set this up so that middle matches dimensions
			// with outer via 100%, so if the outer becomes larger as a result
			// of the text's font loading, middle will have its viewer-area 
			// become larger, making the position of the top border of the
			// viewer (its scrolled position) closer to the top of the
			// (scrolled) content, firing the onscroll event.
			container.style.cssText = "position:absolute; top: -10000px; overflow:hidden;";
			outer.style.cssText = "position:relative; white-space: nowrap; ";
			middle.style.cssText = "position:absolute; width:100%; height:100%; overflow:hidden;";
			inner.style.cssText = "height: 200px;";
			
			//outer.appendChild( document.createTextNode( String.fromCodePoint( "1088323" ) ) );
			outer.appendChild( document.createTextNode( "\udbe6\udf43" ) );
			
			document.body.appendChild( container );
			
			var oWidth = outer.offsetWidth,
			    oHeight = outer.offsetHeight;
			
			container.style.height = oHeight - 1 + "px";
			container.style.width = oWidth - 1 + "px";
			container.scrollTop  = container.scrollHeight - container.clientHeight;
			container.scrollLeft = container.scrollWidth  - container.clientWidth;
			
			inner.style.height = oHeight + 1 + "px";
			inner.style.width  = oWidth + 1 + "px";
			
			middle.scrollTop  = middle.scrollHeight - middle.clientHeight;
			middle.scrollLeft = middle.scrollWidth  - middle.clientWidth;
			
			container.style.fontFamily = '"SignWriting 2010"';
			
			middle.addEventListener( "scroll", checkLoaded, false );
			container.addEventListener( "scroll", checkLoaded, false );
			// Could add attachEvent resize for nonsupporting browsers, but does
			// that even make sense? Are there any browsers that don't support 
			// the scroll check but do support canvas? Unclear.
			
			
			// console.log( "loadListener complete" );
			function checkLoaded() {
				// console.log( "checkLoaded", this );
				if ( !fontHandler.loaded ) {
					// console.log( "loaded - size change" );
					loadedFonts();
				}
			}
			
			function loadedFonts() {
				var d = size( "S2ff00" );
				if ( d !== "" ) {
					document.body.removeChild( container );
					if ( d === "36x35" ) {
						for ( var i = 0; i < SWKB.signAreas.length; i++ ) {
							//SWKB.signAreas[ i ].useFont();
						}
					}
					return true;
				} else {
					return false;
				}
			}
			
			function noFonts() {
				document.body.removeChild( container );
			}
			
			var checked = 0;
			// And if that didn't work, just ping it every once in a while.
			( function periodicChecking () {
				if ( !fontHandler.loaded && checked++ < 10 ) {
					loadedFonts() || setTimeout( periodicChecking, 3000 );
				}
			} )();
		}
		
		function init() {
			if ( !initialized && !config.noFont ) {
				initialized = true;
				canvas = document.createElement( "canvas" );
				context = canvas.getContext && canvas.getContext( "2d" );
				bound = 152;
				
				if ( !context ) {
					// Doesn't support canvas, so we can't use the fonts.
					return;
				}
				
				canvas.height = canvas.width = bound;
				
				context.font = "60px 'SignWriting 2010'";
				
				switch ( size( "S2ff00" ) ) {
					case "36x35" :
						// console.log( "already loaded", size( "S2ff00" ) );
						//config.useFont = true;
						break;
					case "":
						loadListener();
				}
			}
		}
		
		return {
			size: size,
			code: code,
			getImage: getImage,
			init: init,
			loaded: false
		};
	})();
	
	// ** TUTORIAL **
	/*
	Okay, so I'm thinking, we start with ASL "Deaf", because it's a simple sign
	that has one easy handshape, one head, and two duplicated movement symbols.
	Can have things like "press the <green span>movement arrows</span> to 
	position the symbol".
	Start with both the right hand index and the left hand head highlighted in
	colors. Whichever's done first, explain the "Next symbol", and then go to
	the contact symbol. Use Shift+NextSymbol to duplicate, then position, then
	try and simulate autocomplete, maybe. Then explain pressing space. 
	After, maybe try explaining arrow keys.
	Hm, I missed using next symbol to cycle over existing symbols. ...
	
	Random: Ideas for general SW tutorial:
	- Use animations before movement symbols are explained. They're awesome.
	- Maybe video "overlays". Actually, the SW should be the overlay.
	- Things like a contact symbol springing up at the point of contact in a video.
	- 
	
	*/
	
	function tutorial() {
		// Yikes, this became a huge mess very quickly. I probably should have
		// used an existing library for this. Oh well. 
		
		//
		
		// Remember to ask for translations.
		var texts = {
		welcome0 : "Hi! Welcome to the SignWriting Keyboard tutorial.",
		welcome1 : "We'll be going through the steps needed to write the " +
		"sign currently shown in gold above.",
		// M544x530S20500509x514S10011523x500S20500520x487S2ff00482x482
		// Hm, maybe just have the sign overlayed in faded color (pseudosymbols),
		// and not have it inserted into the text.
		welcome2 : "This keyboard has each side of the keyboard running more or less separately." +
		"That is, the left side and the right side are each controlling one " + 
		"symbol at any given time, and you can move and modify these two symbols " +
		"independently of each other.",
		welcome3 : "First, create a head symbol and a right hand symbol with the index " + // Maybe.
		"finger raised. This can be done in whatever order you want.",
		createHead    : "Type $1 to create the head symbol.",
		pressNext1    : "The head is already positioned correctly, so we can " + 
						"go on to the next symbol.",
		pressNext2    : "Press the Next Symbol ($1) key to start another symbol.",
		createHand    : "Type $1 to create the hand symbol with the index finger raised.",
		// Okay, maybe rewrite as a list so we can have check marks.
		positionHand  : "Now, position the hand by doing the following:",
		moveHand      : "Move the hand so that the finger tip is near the " + 
						"right side of the head by using the movement arrow " +
						"keys ($1).",
		faceHand      : "Turn the hand sideways by using the face button ($1).",
		rotateHand    : "Rotate the hand so that the finger points up and to " +
						"the left by using the rotate keys ($1).",
		createArrow   : "Next, we'll create a contact symbol. Contact " + 
						"symbols are in the movement section, so press the $1 key.",
		createContact : "Press the $1 key to create a contact symbol.",
		moveContact   : "Position the contact symbol slightly below and to " + 
						"the right of the head using the movement arrow keys ($1).",
		dupContact    : "Create the second contact symbol. You can duplicate " +
						"your current contact symbol by holding Left Shift " + 
						"while pressing $1.",
		moveDup       : "Now position the second contact symbol just to the " +
						"right of the head, again using the movement arrow " +
						"keys ($1). (The new contact symbol started in the " +
						"same location as the old one, so you'll need to " +
						"move it up and a bit to the right.)",
		forgotShift   : "It seems that you pressed next symbol without " + 
						"holding shift, switching you to the wrong symbol. " + 
						"Press the Next Symbol ($1) key again until you're " + 
						"back on the right symbol.",
		deleteTryAgain: "Hm, it looks like you made a mistake and created " + 
						"the wrong symbol. Press the Delete Symbol key ($1) " + 
						"and try again."
		};
		
		function partitionedBox( i ) {
			var box = document.createElement( "div" );
			// Replace with CSS, when possible.
			box.style.width = "48%";
			box.style.float = "left";
			box.style.padding = "1%";
			div.appendChild( box );
			return box;
		}
		
		var div = document.createElement( "div" ),
			wideBox = div.appendChild( document.createElement( "div" ) ),
			boxes, 
			activeObjectives = [ [], [] ],
			objectives = {
				// Okay, I'm restructuring this to get rid of "next".
				// Instead, check will return the relevant objective where
				// applicable. Thus, I can have things like:
				// "User clicked wrong button > Tell them to fix thing"
				createHead: { // Create head
					keys: 38,
					color: "#FF8800",
					text: texts.createHead,
					check: function ( symb ) {
						return symb.type !== symbolTypes.NONE && 
							( symb.symbolText === "S2ff00" ?
								objectives.pressNext :
								objectives.deleteTryAgain( objectives.createHead, 0 ) ) ;
					},
					done: function ( p ) {
						clear( wideBox );
					}
				},
				pressNext: { // Press next on left hand
					keys: 30,
					color: "#FF8800",
					text: [ texts.pressNext1, texts.pressNext2 ],
					check: function ( symb ) {
						if ( symb.symbolText !== "S2ff00" ) {
							return symb.type === 0 ?
								objectives.createArrow :
								objectives.deleteTryAgain( objectives.createHead, 0 );
						}
					}
				},
				createHand: { // Create right hand
					keys: 32,
					color: "#0000FF",
					text: texts.createHand,
					check: function ( symb ) {
						if ( symb.type !== symbolTypes.NONE ) {
							return symb.fingers === "100" ?
								objectives.positionHand :
								objectives.deleteTryAgain( objectives.createHand, 1 );
						}
					},
					done: function () {
						clear( wideBox );
					}
				},
				positionHand: { // General hand positioning, with subs
					text: texts.positionHand,
					subs: function () {
						return [ objectives.faceHand, objectives.rotateHand, objectives.moveHand ];
					},
					check: function ( symb ) {
						if ( symb.fingers === "100" ) {
							var a = activeObjectives[ 1 ];
							return a[ 1 ].complete && a[ 2 ].complete && a[ 3 ].complete &&
								objectives.allDone;
						} else {
							// return objectives.deleteTryAgain( objectives.createHand, 1 );
						}
					},
					done: function () {
						var a = activeObjectives[ 1 ];
						while( a.length ) {
							a.pop();
						}
					}
				},
				faceHand: { // Hand face sideways
					keys: 19,
					color: "#00FF00",
					text: texts.faceHand,
					check: function ( symb ) {
						return symb.face === 1;
					}
				},
				rotateHand: { // Rotate hand 5
					keys: [ 10, 11 ],
					color: "#0000FF",
					text: texts.rotateHand,
					check: function ( symb ) {
						return symb.rotation === 1;
					}
				},
				moveHand: { // Position hand
					keys: [ 9, 20, 21, 22 ],
					color: "#00FFFF",
					text: texts.moveHand,
					check: function ( symb ) { // 523x500
						return symb.X > 518 && symb.X < 528 &&
							symb.Y > 495 && symb.Y < 505;
					}
				},
				createArrow: { // Movement symbol
					keys: 39,
					color: "#FF8800",
					text: texts.createArrow,
					check: function ( symb ) {
						if ( symb.type !== symbolTypes.NONE ) {
							return symb.type === 2 ?
								objectives.createContact :
								objectives.deleteTryAgain( objectives.createArrow, 0 );
						}
					}
				}, 
				createContact: { // Contact symbol
					keys: 29,
					color: "#FF8800",
					text: texts.createContact,
					check: function ( symb ) {
						return symb.symbolText === "S20500" &&
							objectives.moveContact;
					}
				},
				moveContact: { // Move Contact symbol
					keys: [ 4, 14, 15, 16 ],
					color: "#FF8800",
					text: texts.moveContact,
					check: function ( symb ) { // 509x514, 520x487
						if ( symb.symbolText !== "S20500" ) {
							return objectives.deleteTryAgain( 
								objectives.createArrow, 0 );
						}
						return symb.X > 504 && symb.X < 514 &&
							symb.Y > 509 && symb.Y < 519 &&
							objectives.dupContact;
					}
				},
				dupContact: { // Duplicate contact symbol
					keys: 30,
					color: "#FF8800",
					text: texts.dupContact,
					check: function ( symb ) {
						var a = symb, 
							symbols = activeSign.symbols,
							firstStar;
						// If we clicked next without shift, explain.
						
						for ( var i = 0; i < symbols.length; i++ ) {
							if ( symbols[ i ].symbolText === "S20500" ) {
								firstStar = symbols[ i ];
								break;
							}
						}
						if ( a.symbolText === "S20500" ) {
							if ( firstStar !== a ) {
								// Successfully duplicated
								return objectives.moveDup;
							} else {
								// Nothing happened.
							}
						} else {
							if ( firstStar ) {
								// User hit "next", without shift. Explain hit
								// next until we're back.
								return objectives.forgotShift;
							} else {
								// User modified the star. Explain to delete.
							}
						}
					}
				},
				forgotShift: {
					keys: 30,
					color: "#FF8800",
					text: texts.forgotShift,
					check: function ( symb ) {
						if ( symb.symbolText === "S20500" ) {
							return objectives.dupContact;
						}
					}
				},
				moveDup: { 
					keys: [ 4, 14, 15, 16 ],
					color: "#FF8800",
					text: texts.moveDup,
					check: function ( symb ) { // 509x514, 520x487
						return symb.X > 515 && symb.X < 525 &&
							symb.Y > 482 && symb.Y < 492 &&
							objectives.allDone;
					}
				},
				deleteTryAgain: function ( obj, side ) {
					return {
						keys: side ? 12 : 1,
						color: side ? "#0000FF" : "#FF8800",
						text: texts.deleteTryAgain,
						check: function ( symb ) {
							return symb.type === symbolTypes.NONE && obj;
						}
					}
				},
				allDone: {
					text: "",
					check: function () {
						return activeObjectives[ 0 ][ 0 ] === 
							activeObjectives[ 1 ][ 0 ];
					},
					done: function () {
						clear( boxes[ 0 ] ); clear( boxes[ 1 ] );
						addDialogue( wideBox, "All done! Press space to start a new sign. ", "p" );
						addDialogue( wideBox, "(That's all I've written of " + 
							"this tutorial so far. I might expand on this further later.)", "p" );
					}
				}
			};
		
		function startObjective( objective, side, pNode, nT ) {
			
			var text = objective.text || [];
			if ( typeof text === "string" ) {
				text = [ text ];
			}
			for ( var i = 0; i < text.length; i++ ) {
				objective.box = addDialogue( 
					pNode || boxes[ side ],
					text[ i ], 
					nT || "p", 
					objective.keys, 
					objective.color
				);
			}
			activeObjectives[ side ].push( objective );
			if ( objective.subs ) {
				var ul = boxes[ side ].appendChild( document.createElement( "ul" ) );
				for ( var subs = objective.subs(), i = 0; i < subs.length; i++ ) {
					//objective
					startObjective( subs[ i ], side, ul, "li" );
				}
			}
		}
		
		function finishObjective( obj, side ) {
			actObj = activeObjectives[ side ];
			if ( actObj[ 0 ] === obj ) {
				for ( ; actObj.pop(); ) {
					
				}
				clear( boxes[ side ] );
			} else {
				var check = obj.checkMark || document.createElement( "span" );
				if ( !obj.checkMark ) {
					check.appendChild( document.createTextNode( "✔" ) );
					check.style.color = "#00CC00";
					obj.box && obj.box.insertBefore( check, obj.box.firstChild );
				}
				obj.checkMark = check;
			}
			
			obj.complete = true;
			obj.done && obj.done();
			if ( obj.keys ) {
				for ( var i = obj.keys.length || 1; i--; ) {
					keyboard.colorKey( obj.keys[ i ] || obj.keys );
				}
			}
		}
		
		function addDialogue( box, text, inNode, keyPosition, color ) {
			var span, style;
			text = text.split( "$1" );
			if ( inNode ) {
				box = box.appendChild( document.createElement( inNode ) );
			}
			box.appendChild( document.createTextNode( text[ 0 ] ) );
			if ( text[ 1 ] ) {
				for ( var i = 0; i < ( keyPosition.length || 1 ); i++ ) {
					if ( i > 0 ) {
						box.appendChild( document.createTextNode( ", " ) );
					}
					span = box.appendChild( document.createElement( "span" ) );
					style = span.style;
					style.color = color;
					style.fontWeight = "bold";
					span.appendChild( 
						document.createTextNode( 
							keyboard.getLetter( keyPosition[ i ] || keyPosition )
						 )
					);
					keyboard.colorKey( keyPosition[ i ] || keyPosition, color );
				}
				box.appendChild( document.createTextNode( text[ 1 ] ) );
			}
			return box;
		}
		
		function clear( elem ) {
			for ( ; elem.firstChild; ) {
				 elem.removeChild( elem.firstChild );
			}
		}
		
		function tutorialTrackKeys( e ) {
			
			function checkIfComplete( i, ii, obj ) {
				var next, done;
				
				next = obj.check && obj.check( activeSymbols[ !!i ] );
				if ( next ) { // Completed the objective?
					finishObjective( obj, i );
					
					if ( ii === 0 ) {
						startObjective( next, i );
					}
					
				} else if ( obj.checkMark ) {
					obj.box.removeChild( obj.checkMark );
					delete obj.checkMark;
				} else {
					done = false;
				}
			}
			
			for ( var i = activeObjectives.length, obj, newObj; i--; ) {
				for ( var ii = activeObjectives[ i ].length; ii--; ) {
					//checkIfComplete( i, 0, activeObjectives[ i ][ 0 ] );
					checkIfComplete( i, ii, activeObjectives[ i ][ ii ] );
				}
			}
		}
		
		div.style.position = "absolute";
		div.style.bottom = "215px";
		div.style.padding = "0 125px";
		div.style.width = "650px";
		div.style.minHeight = "100px";
		div.style.fontSize = "16px";
		div.style.backgroundColor = "#FFFFFF";
		div.style.zIndex = "50";
		div.style.border = "2px solid #AAA";
		div.style.borderRadius = "6px";
		
		for ( var i = 0; i < 4; i++ ) {
			addDialogue( wideBox, texts[ "welcome" + i ], "p" );
		}
		boxes  = [ partitionedBox( 0 ), partitionedBox( 1 ) ];
		
		startObjective( objectives.createHead, 0 );
		startObjective( objectives.createHand, 1 );
		document.body.appendChild( div );
		
		var pseudos = [ 
			"S20500509x514", "S10011523x500", "S20500520x487", "S2ff00482x482"
		];
		for ( var i = 0; i < pseudos.length; i++ ) {
			activeSign.pseudos.push( new PseudoSymbol( pseudos[ i ], "FFD700" ) );
		}
		
		activeSign.updatePosition();
		
		addEventListener( document, "keydown", tutorialTrackKeys );
		addEventListener( document, "keyup",   tutorialTrackKeys );
		addEventListener( keyboard.element, "mousedown", tutorialTrackKeys );
		addEventListener( keyboard.element, "mouseup",   tutorialTrackKeys );
	}
	
	// ** QUNIT STUFF **
	// Some qunit tests. This will be moved somewhere else later, or deleted.
	// I have no idea what I'm doing here.
	// Currently nonfunctional. Tests will not pass. 
	function SWtest() {
		mw.loader.using( [ "jquery.qunit" ], function () {
			
			$( 'body' ).append( 
				$( "<div>" )
					.attr( "id", "qunit" )
					.css({
						position: "fixed",
						bottom: 0,
						right: 0,
						zIndex: 20,
						width: "500px",
						maxHeight: "600px",
						overflow: "auto",
						webkitWritingMode: "horizontal-tb"
					})
			);
			
			//QUnit.load();
			
			module( "SignWriting Keyboard", {
				setup: function(){ 
					//new SignArea(); 
					
					// TODO: Set up a blank text box, to get consistent results.
					
				},
				teardown: function(){}
			} );
			
			function trigger( type, element, which, props ) {
				var event = document.createEvent('Event');
				event.initEvent( type, true, true );
				if ( which ) {
					event.keyCode = which;
				}
				if ( props ) {
					for ( var i in props ) {
						event[ i ] = props[ i ];
					}
				}
				( element || document ).dispatchEvent( event );
			}
			
			function pressKey( key, props ) {
				var code;
				if ( typeof key !== "number" ) {
					for ( var i in keyCodeTable ) {
						if ( keyCodeTable[ i ] === key ) {
							code = i;
							break;
						}
					}
					code =  keyboard.keyMap.layout[ 
						keyMaps.qwerty.layout.indexOf( code + '' )
					];
				} else {
					code = key;
				}
				// updateKey( code, true );
				// updateKey( code, false );
				trigger( "keydown", document.body, code, props );
				trigger( "keyup", document.body, code, props );
			}
			
			test( "Basic hand symbols", function ( assert ) {
				var symbol = new SignSymbol(); // lefthand
				symbol.type = symbolTypes.HAND;
				symbol.fingers = "111";
				symbol.updateImage();
				assert.equal( symbol.signText, "S11128492x485", "Lefthand unbent signtext" );
				symbol = new SignSymbol();
				symbol.type = symbolTypes.HAND;
				symbol.rightHand = true;
				symbol.fingers = "129";
				symbol.updateImage();
				assert.equal( symbol.signText, "S12920492x485", "Righthand bent signtext" );
				pressKey( "A" );
				assert.equal( activeSymbols[ false ].type, symbolTypes.HAND, "Keyboard" );
				assert.ok( activeSign.element.contains( activeSymbols[ false ].element ), "Element in DOM" );
			});
			
			test( "Basic head symbols", function ( assert ) {
				pressKey( "," );
				pressKey( "J" );
				pressKey( "J" );
				assert.equal( activeSymbols[ true ].type, symbolTypes.HEAD, "Type" );
				assert.equal( activeSymbols[ true ].signText, "S32100482x482", "signtext" );
				activeSymbols[ true ].type !== symbolTypes.NONE && pressKey( "=" );
			});
			
			test( "Fingerspelling mode", function ( assert ) {
				pressKey( "]" );
				pressKey( "A" );
				var firstSymbol = activeSymbols[ true ];
				assert.equal( firstSymbol.symbolText, "S1f720", "Symboltext" );
				assert.equal( firstSymbol.Y, "450", "Vertical positioning" );
				pressKey( "B" );
				pressKey( 8 ); // Backspace
				assert.equal( firstSymbol, activeSymbols[ true ], "Backspace" );
				pressKey( "]" );
			});
			
			test( "Signs", function ( assert ) {
				var sign = activeSign, secondSign;
				pressKey( 32 ); // Space
				// TODO: Check centering
				assert.notEqual( sign, activeSign, "Spacekey - change activeSign" );
				assert.equal( secondSign = signArea.signs[ 1 ], activeSign, "Sign in signList" );
				assert.ok( document.body.contains( activeSign.element ), "Element in DOM" );
				assert.equal( activeSign.toString(), "", "Empty sign is blank" );
				pressKey( "6" );
				assert.equal( activeSign.lane, "L", "Lanes" );
				pressKey( 38 ); // Down key
				assert.equal( activeSign, sign, "Arrow keys" );
				trigger( "click", secondSign.element );
				assert.equal( activeSign, secondSign, "Clicking to select sign" );
				// pressKey( 40 ); // Up key
				pressKey( 8 ); // Backspace
				assert.equal( activeSign, sign, "Backspace - return activeSign" );
			});
			
			if ( !$( "#wpTextbox1" )[ 0 ] ) {
				return;
			}
			
			test( "SignAreas", function ( assert ) {
				$( "#wpTextbox1" ).val("");
				trigger( "focus", $( "#wpTextbox1" )[ 0 ] ); // Focus textbox.
				assert.ok( signArea instanceof SignArea, "Created instance" );
				var sign = activeSign, area = signArea;
				assert.notEqual( signArea.signs.indexOf( activeSign ), -1, "Created and focused sign" );
				trigger( "focus", $( "#searchInput" )[ 0 ] );
				assert.equal( sign.element.className, "SWKB-Sign", "Blurred old sign" );
				assert.notEqual( sign, activeSign, "Switched signs" );
				trigger( "click", sign.element );
				assert.equal( area, signArea, "Switched by click" );
				//assert.equal( signArea.signs[ 0 ], activeSign, "Autofocused sign on area focus" );
				// ^not sure what's up with that one
			});
			
			if ( config.ce ) {
				pressKey( "A" );
				test( "NonSignText", function ( assert ) {
					pressKey( "`" ); // Toggle plain text
					assert.ok( activeSign instanceof NonSignText, "Created" );
					pressKey( 38 ); // Up key
					assert.ok( activeSign.isSign, "Arrow key up > focus previous sign" );
					pressKey( 40 ); // Down key
					assert.equal( window.getSelection().focusNode, activeSign.element, "Caret positioned on focus" );
					activeSign.element.innerText = "aa";
					assert.equal( activeSign.toString(), "aa", "Basic toString" );
					pressKey( 40 );
					assert.equal( window.getSelection().focusOffset, 1, "Arrow key moves caret" );
					var length = signArea.signs.length;
					pressKey( "`" );
					assert.equal( signArea.signs.length, length + 2, "Toggle-plaintexts can split NonSignText" );
					pressKey( 38 ); 
					assert.equal( window.getSelection().focusOffset, 1, "Focus end of NST when moving caret backward" );
					pressKey( 40 );
					assert.equal( activeSign, signArea.signs[ length ], "Switch sign from NST on down arrow" );
					pressKey( "`" );
					assert.equal( activeSign, signArea.signs[ length - 1 ], "Handle togglePlaintext on empty sign - focus" );
					assert.equal( signArea.signs.length, 2, "Handle togglePlaintext on empty sign - merge" );
					assert.equal( window.getSelection().focusOffset, 1, "Handle togglePlaintext on empty sign - caret" );
					pressKey( 38 );
					pressKey( 8 ); // Backspace
					assert.equal( signArea.signs[ 0 ], activeSign, "Backspace at start of NST focuses previous" );
					pressKey( 40 );
					activeSign.element.innerText = "";
					pressKey( 8 );
					assert.equal( signArea.signs.length, 1, "Backspace removes empty NST" );
					pressKey( "`" );
					pressKey( "`" );
					assert.equal( signArea.signs.length, 2, "togglePlaintext removes empty NST" );
					
					// Not so easily testable issues include backspace behaviour
					// issues, DOM appearance, mouse clicks (selecting)...
				});
			}
			
		} ).done( function () {
			QUnit.load();
		});
	}
	
	// ** INITIALIZATION **
	function init() {
		
		
		/*
		// For Wikimedia, disable ime, as it's quite harmful to performance.
		// Please note that ime disabling is a thoroughly absurd process that
		// is completely impossible to do in a simple manner. 
		// This is not an elegant solution, but it's as elegant as it's going
		// to get. Seriously, the previous attempt at doing this involved a
		// collection of global-var changes/deletions, several event listener
		// modification attempts, and a whole bunch of loader and settimeout 
		// stuff. mw.ime.disable doesn't usually work by itself (bug 71682), and
		// doesn't even exist until much of ime's loaded, which doesn't happen 
		// until an element is focused. Here be dragons. Do not touch these 
		// lines until either the bug is fixed, or we're ready to leave the
		// incubator with this code.
		// Thanks to Ori for the fix.
		*/
		if ( window.mw && window.$ ) {
			mw.ime && mw.ime.disable && mw.ime.disable();
			$.ime && $.ime.preferences && $.ime.preferences.disable &&
				$.ime.preferences.disable();
			mw.loader.state( [ 'ext.uls.ime', 'jquery.ime' ], 'missing' );
			$('.imeselector').remove();
			$( 'body' ).off( '.ime' ).off( '.imeinit' );
		}
		
		// Initialize the preferences object, the layout, and the keyboard, in
		// that order, per dependencies.
		prefs.init();
		
		layout = getLayout( config.enableDOSMode ? 
			DOSLayout : 
			( prefs.get( "layout" ) || BaseLayout )
		);
		
		// At some point, I'm going to set this up so that the keyboard will 
		// only be constructed after the first SignArea is built.
		keyboard.init();
		
		// Create fakeSymbol, which is used to simulate key presses.
		fakeSymbol = new SignSymbol();
		fakeSymbol.fake = true;
		
		// To be removed at some point: Set up a new editing area right away,
		// right in the body.
		if ( config.autostart ) {
			new SignArea();
			
			// This line should be removed at some point. Focusing the new SignArea
			// should update automatically.
			keyboard.update();
		} else {
			keyboard.hide();
		}
		
		// Set up listeners to create SignAreas for input boxes and textareas.
		setupTextboxListeners();
		
		// Listen for resizes, so that we can resize the SAs appropriately.
		addEventListener( window, "resize", function () {
			if ( !disabled ) {
				/*
				for ( var i = 0; i < signAreas.length; i++ ) {
					signAreas[ i ].setSize();
				}
				*/
				caret._docWidth = undefined;
			}
		});
		
		// Purge getClientRects data on scroll.
		addEventListener( window, "scroll", function () {
			if ( signArea ) {
				signArea.coords = undefined;
			}
		});
	}
	
	//$("#wpTextbox1").val('');
	init();
	location.search.indexOf( "SWKB-tutorial" ) !== -1 && tutorial();
	//SWtest();
	
	return {
		// Maybe add symbolTypes and/or layout here?
		// symbolTypes: symbolTypes, 
		// Also tutorial, maybe.
		// For testing
		test: SWtest, // Unit testing
		config: config,
		keyboard: keyboard,
		signAreas: signAreas,
		generalActions: generalActions,
		symbolTypes: symbolTypes,
		fswParser: fswParser,
		activeSymbols: activeSymbols,
		//contentTypes: { SignArea, Sign, SignSymbol, NonSignText },
		disable: function () {
			signArea && signArea.blur();
			disabled = true;
			for ( var i = 0; i < signAreas.length; i++ ) {
				signAreas[ i ].hide();
			}
		},
		changeLayout: changeLayout,
		loaded: true
	};
	
})();