11 KiB
metadata.title = "Type: A FOSS clone of typing.io"
metadata.tags = ["misc"]
metadata.date = "2016-10-08 17:29:42 -0400"
metadata.oldPermalink = ["/misc/2016/10/08/type/", "/misc/2016/type/"]
metadata.shortDesc = "I made an awesome FOSS clone of typing.io that you can check out at type.shadowfacts.net."
TL;DR: I made an awesome FOSS clone of typing.io that you can check out at type.shadowfacts.net and the source of which you can see here.
I've used typing.io on and off for almost a year now, usually when I'm bored and have nothing else to do. Unfortunately, I recently completed the Java file, the C++ file, and the JavaScript file (that last one took too much time, jQuery has weird coding formatting standards, IMO) meaning I've completed pretty much everything that interests me. Now if you want to upload your own code to type, you have to pay $9.99 a month, which, frankly, is ridiculous. $10 a month to be able to upload code to a website only to have more than the 17 default files (one for each langauge) when I could build my own clone.
This is my fourth attempt at building a clone of typing.io, and the first one that's actually been successful. (The first, second, and third all failed because I was trying to make a syntax highlighting engine work with too much custom code.)
Type uses CodeMirror, a fantastic (and very well documented) code editor which handles syntax highlighting, themes, cursor handling, and input.
Input
Input was one of the first things I worked on. (I wanted to get the very basics working before I got cought up in minor details.) CodeMirorr's normal input method doesn't work for me, because in Type, all the text is in the editor beforehand and the user doesn't actually type it out. The CodeMirror instance is set to readOnly
mode, making entering or removing text impossible. This is all well and good, but how can you practice typing if you can't type? Well, you don't actually type. The DOM keypress
and keydown
events are used to handle character input and handle special key input (return, backspace, tab, and escape) respectively.
The keypress
event handler simply moves the cursor one character and marks the typed character as completed. If the character the user typed isn't the character that's in the document they are typing, a CodeMirror TextMarker with the invalid
class will be used to display a red error-highlight to the user. These marks are then stored in a 2-dimensional array which is used to check if the user has actully completed the file.
The keydown
event is used for handling special key pressed namely, return, backspace, delete, and escape.
When handling a return press, the code first checks if the user has completed the current line (This is a little bit more complicated than checking if the cursor position is at the end of the line, because Type allows you to skip typing whitespace at the beggining and end of lines because every IDE/editor under the sun handles that for you). Then, the editor moves the cursor to the beggining of the next line (see the previous parenthetical).
Backspace handling works much the same way, checking if the user is at the begging of the line, and if so, moving to the end of the previous line, or otherwise moving back 1 character. Delete also has a bit of extra functionality specific to Type. Whenever you press delete and the previous character was marked as invalid, the invalid marks needs to A) be cleared from the CodeMirror document and B) removed from the 2D array of invalid marks that's used for completion checking.
The tab key requires special handling because it's not entered as a normal character and therefore special checking has to be done to see if the next character is a tab character. Type doesn't handling using the tab key with space-indentation like most editors/IDEs because most places where you'd encounter significant amounts of whitespace in the middle of a line, it's a tab character used to line up text across multiple lines.
Escape is handled fairly simply. When escape is pressed and the editor is focused, a global focus
variable is toggled, causing all other input-handling to be disabled, a Paused
label is added/removed in the top right of the top bar, and lastly the paused
class is toggled on the page, which, when active, gives the editor 50% opacity, giving it a nice effect that clearly indicates the paused state.
Cursor Handling
Preventing the cursor movement (something you obviously don't want for a typing practice thing) that's still possible, even in CodeMirror's read only mode, is accomplished simply by adding an event listener on the CodeMirror mousedown
event and calling preventDefault
on the event to prevent the editor's default behavior from taking place and calls the focus
method on the editor instance, focusing it and un-pauses it if it was previously paused.
Syntax Highlighting
Syntax highlighting is handled completely using CodeMirror's modes, so Type supports* everything that CodeMirror does. By default, Type will try to automatically detect a language mode to use based on the file's extension, falling back to plain-text if a mode can't be found. This is accomplished by searching the (ridiculously large and manually written) langauges map that stores A) the JS CodeMirror mode file to load, B) the MIME type to pass to CodeMirror, and C) the extensions for that file-type (based on GitHub linguis data). Yes, I spent far too long manually writing that file when I probably could have automated it. The script for the mode is then loaded using jQuery.getScript
and the code, along with the MIME type, and a couple other things, are passed into CodeMirror.fromTextArea
.
* Technically it does, however only a subset of those languages can actually be used because they seem common enough** to warrant being manually added to the languages map.
** I say "common" but Brainfuck and FORTRAN aren't really common, I just added them for shits and giggles.
Themes
Themes are handled fairly similarly to syntax highlighting. There's a massive <select>
dropdown which contains all the options for the CodeMirror themes. When the dropdown is changed, the stylesheet for the selected theme is loaded and the setTheme
function is called on the editor.
Chunks
Chunks were the ultimate solution to a problem I ran into fairly early on when I was testing Type. Due to the way Type handles showing which parts of the file haven't been completed (having a single TextMarker
going from the cursor to the end of the file and updating it when the cursor moves), performance suffers a lot for large files because of the massive amount of DOM updates and re-renders when typing quickly. The solution I came up with was splitting each file up into more managable chunks (50 lines, at most) which can more quickly be re-rendered by the browser. Alas, this isn't a perfect solution because CodeMirror's lexer can sometimes break with chunks (see Fixing Syntax Highlighting) , but it's the best solution I've come up with so far.
Storage
One of the restrictions I imposed on myself for this project (mostly because I didn't want to pay for a server) was that Type's functionality had to be 100% client-side only. There are two primary things that result from this 1) Type is account-less and 2) therefore everything (progress, current files, theme, etc.) have to be stored client-side.
I decided to use Mozilla's localForage simply because I remembered it when I had to implement storage stuff. (If you don't know, localForage is a JS wrapper around IndexedDB and/or WebSQL with a fallback to localStorage which makes client-side persistence much nicer.)
Basic overview of what Type stores:
root | +-->theme | +-->owner/repo/branch | +-->path/to/file | +-->chunk | +-->chunks array | +-->0 | +-->cursor | | | +-->line | | | +-->ch | +-->elapsedTime | +-->invalids array | +-->invalids array | +-->0 | +-->line | +-->ch
If you want to see actually what Type stores, feel free to take a look in the Indexed DB section of the Application tab of the Chrome web inspector (or the appropriate section of your favorite browser).
WPM Tracking
WPM tracking takes place primarily in the updateWPM
function which is called every time the user presses return to move to the next line. updateWPM
does a number of things.
- If the editor is focused, it updates the elapsed time.
- It gets the total number of words in the chunk. This is done by splitting the document text with a regex that matches A) any whitespace character B) a comma C) a period and getting the length of the resulting array.
- Getting the total number of minutes from the elapsed time (which is stored in miliseconds).
- The WPM indicator is updated (# of words / # of minutes).
What's Next
Right now, Type is reasonably complete. It's in a perfectly useable state, but there are still more things I want to do.
Fixing Syntax Highlighting
Because of the chunking system, in some cases syntax highlighting is utterly broken because a key thing that the lexer needs to understand what the code is isn't present because it's in the previous chunk. One relatively common example of this is block-comments. If a block-comment begins in one chunk but terminates in a later chunk, the text that's inside the comment but in a different chunk than the starting mark has completely invalid highlighting because the lexer has no idea it's a comment.
Skipping Comments
This is a really, really nice feature that typing.io has which is that as you're typing, the cursor will completely skip over comments, both on their own lines and on the ends of other lines. This should be possible, I just need to hook into CodeMirror's syntax highlighting code and find out if the next thing that should be typed is marked as a comment and if so, skip it.
Polishing
If you've looked at the site, you can tell it's fairly unpolished. It's got almost no styling and is fairly unintuitive. It'll probably remain minimalistic, but it'd be nice to have unified design/theme across the entire site.
Typo Heatmap
This is a feature in the premium version of typing.io that I'd like to add to Type. It shows a heat map of all the keys you make errors on. The only thing that's preventing me from working on this currently is it would require manually writing a massive data file containing all the locations of all the keys and which characters correspond to which keys, something I don't want to do after spending too much time manually writing the language map.