Rapid Prototyping: Highlighty
🏍️

Rapid Prototyping: Highlighty

Published
May 18, 2019
Tags
Articles
Author
Stephen Wu
Over this past fall term, I launched an open-source Chrome & Firefox extension called Highlighty to help people highlight phrases from provided phrase lists. This article breaks down some of the development process and shows how to build an MVP version.
This article is for: people who want to learn more about project or web development and people who want to contribute to Highlighty, with some basic web development experience (HTML/CSS/JavaScript).
Highlighty in action!
Highlighty in action!

Highlighty

Conception

This project was born out of an internal desire at Facebook, where I previously interned in the fall. An employee made a post on the internal Workplace about alternative highlighting extensions. Surprisingly a lot of people at FB have highlighting needs, like recruiters or community specialists, so I took on the project as a side project. I started out with a TampermonkeyÂą script and then iterated it into a web extension.
Two popular extensions for highlighting on Chrome are Multi-highlight (167K users) and Highlight This (41K users). Multi-highlight lacked multiple phrase list support and several other features. Highlight This was strong feature-wise but didn’t have a great UI. Both weren’t open-source projects. I wanted to improve upon both and make the project open-source, so others could contribute and chime in on desired features, with security and privacy as a priority.
[1] Greasemonkey and Tampermonkey are browser extensions that let you install userscripts, which are basically mini-browser extensions that run JavaScript. They’re nice for rapid development of lightweight scripts like this use-case but bad at adding GUIs and being user-friendly. I wrote an initial version of Highlighty using Tampermonkey.

Development

OK – let's start by building an MVP (minimum viable product) of a phrase highlighter extension.

Tools

We’re starting out with two tools: jQuery and mark.js.
 
notion image
notion image

jQuery

  • jQuery extends JavaScript and provides lots of useful functions to access the DOM in an easier way.
Some jQuery basics (skip if familiar):
/* $(document).ready - wait for document to fully load! */ $(document).ready(function() { alert("The document is loaded!"!); } // or $(function() { alert("The document is loaded!"!); } /* Some $(selector) methods - The main way to specify what document element(s) to target. Add triggers, styling, classes, and more. - $(`selector`) can be like: `.column` (every element with the class column) `span` (every <span> tag) `#Settings__enableTitleMouseover` (every element with this ID) - Remember, `.` means class and `#` means ID - This is equivalent to `document.getElementById()` or `.getElementByClassName()`, but easier to write */ // on(): create a trigger, e.g. "click", "mouseover" $("button#submit").on("click", function() { alert("You clicked a button!") }); // addClass(), removeClass(): add or remove classes $(".dialog").addClass("is-invisible"); ${".dialog").removeClass("is-invisible"); // attr(): change or add an attribute $(".checkbox").attr("checked"); $("#cool-photo").attr("title", "Photo by Steve"); // append(): insert an element to the end of selector $("head").append($("<style>p { color: yellow !important }</style>")); // Convert all <p> tags to yellow, by appending a style to <head> // Note that you'll need the $() wrapping the <style> block // to convert it intto an htmlString from a regular string

mark.js: JavaScript keyword highlighter

  • There’s a lot of complexities and preferences when it comes to locating and marking phrases like: How should you handle cross-element phrases?Âą What about configuration like case-sensitivity or partial word matching?² How do you navigate and manipulate the DOM’s text efficiently?
  • Mark.js solves those and does the actual locate-and-highlighting process for us, doing saving us a lot of work. It also lets us customize the highlighter parameters, so we can let the user answer the questions below.
  • I found Mark.js on Google looking up “highlighting javascript library” and comparing some of the options.
[1] Is covfefe in <div>cov<b>fefe</b></div>?[2] Is super in SUPERCALIFRAGILISTICEXPIALIDOCIOUS?

Development Considerations

Before we get started, here are some of the development choices I made for Highlighty.
  • [JS] Making use of ES6 arrow functions where possible. Basically (param1, param2) => { doStuff() } is similar to function(param1, param2) { doStuff() } with some minor differences.
Here are some ES5 and ES6 basics (skip if familiar):
/* Variable types */ // So we might be used to writing variables like: var someVariable = "helloWorld!"; // But we can use `let` to make a variable block-scoped // It'll only be applicable inside the block it's declared in. let pineapplesBelongOnPizza = true; if (pineapplesBelongOnPizza) { let myFavoriteTopping = "pineapples"; } console.log(myFavoriteTopping); // Error: myFavoriteTopping is undefined! // `const` is like `let` in that it's block-scoped // but it cannot be reassigned or redeclared. const myFavoriteTopping = "pepperoni"; if (pineapplesBelongOnPizza) { myFavoriteTopping = "pineapple"; // Error: 'myFavoriteTopping' has already been declared } // My preference is to always default to `const`, then `let` if needed! // I also use `const` for functions. // See this article: https://link.medium.com/MFxZE2APfU /* ES6 arrow functions make writing functions easier */ function coolFunction(params) { console.log(params); } // is similar to const coolFunction = (params) => { console.log(params); }; // But wait, there's more.. const multiplyByTwo = x => x * 2; multiplyByTwo(-30); // Result: -60 const evens = [2,4,6,8]; const odds = evens.map(v => v + 1); // odds = [3,5,7,9] // Without brackets, you don't need a `return`; it's implicit. // My preference is to always use arrow functions except in some special cases. // Read more: http://tc39wiki.calculist.org/es6/arrow-functions/ /* New `for` loops */ // We're probably used to this: var listOfWords = ["hello", "world", "lorem", "ipsum"]; for (var i = 0; i < listOfWords.length; i++) { console.log(listOfWords[i]); } // But now we can do this: for (let word of listOfWords) { console.log(word); } // or for (let index in listOfWords) { console.log(listOfWords[index]); } // and even... let str = "fetch"; for (let char of str) { console.log(char); } // prints every character in "fetch" to console // The `let` here is optional and personal preference. // Default scoping is block-scope anyways! /* New Object functions */ const obj = { 0: 'a', 1: 'b', 2: 'c' }; Object.entries(obj); // [['0', 'a'], ['1', 'b'], ['2', 'c']] Object.keys(obj); // ['0', '1', '2'] Object.values(obj); // ['a', 'b', 'c'] /* Template literals let you put variables in strings faster */ const pi = 3.1415; console.log(`Pi is ${pi}`); // Putting it all together: const favorites = {topping: "pepperoni", cheese: "mozzarella"}; for (let [key, value] of Object.entries(favorites)) { console.log(`My favorite ${key} is ${value}`); }

Building an MVP!

JSFiddle is a code sandbox tool with separate panes for HTML, CSS, and JavaScript! In this script, we've loaded jQuery and mark.js loaded as external libraries with some text to work with.

Base Script: Highlighting

Before we get to the browser extension, let’s start with creating a minimum viable product (MVP) base script.
Given: List of phrases + User hits a button (e.g. F6) Highlight: All those phrases on the page using mark.js.
With this base script version, we'll pre-define the list of phrases. We’ll make a highlighter object with a list of phrase lists that represents the ideal input we want to handle. Each phrase list has a title, phrases list, and color.
let highlighter = [ { title: "Lorem ipsum words 1", phrases: ["Lorem ipsum", "vestibulum nulla", "elit"], color: "green" }, { title: "Lorem ipsum words 2", phrases: ["sit", "amet", "in", "nunc"], color: "#ba3030" }];
highlighter object: Our goal is to use this object to highlight phrases.
Reading the mark.js documentation, we know we can highlight phrases like this, for every phrase.
$("body").mark("Lorem ipsum");
And that’ll produce a basic yellow highlight on "Lorem ipsum.”
Result of the code above!
Result of the code above!
But we want some more customization, so we’re using $("body").mark("Lorem ipsum", options) where options is the object where we add parameters like className which adds the colors we want. For now, let’s pre-style the colors in the CSS file in JSFiddle, adding a new class to color it any color.
Let’s test with:JS:  $("body").mark("Lorem ipsum", { className: "highlighter" }) CSS: .highlighter { background-color: green; }
We can see that the result is that “Lorem ipsum” is highlighted green.
Notice how "Lorem" and "ipsum" are highlighted separately though? To change that and search for the whole phrase rather than space-delimited words, we’ll add separateWordSearch: false to the options.
So we get:
$("body").mark("Lorem ipsum", { className: "highlighter", separateWordSearch: false });
Now let’s use the entire highlighter object, by looping through each of its phrase lists and marking all of the phrases in them with the class highlighter.
According to the Mark.js docs, we can also do $("body").mark(highlighter.phrases) using an array instead of a string. Now we get to here:
let highlighter = [ { title: "Lorem ipsum words 1", phrases: ["Lorem ipsum", "vestibulum nulla", "elit"], color: "green" }, { title: "Lorem ipsum words 2", phrases: ["sit", "amet", "in", "nunc"], color: "#ba3030" }]; for (let phraseList of highlighter) { $("body").mark(phraseList.phrases, { className: "highlighter", separateWordSearch: false }); }
Result of the code above!
Result of the code above!
Now we can deal with the individual phrase list colors. One way of doing that is to inject the styles into the HTML sheet with jQuery¹. Let’s also get rid of the .highlighter CSS from earlier that made things green.
We can do this simultaneously with marking the individual words while looping through the highlighter object, but we need to give phrases different classes depending on their list. Let's give each phrase the class .highlighter-(index) where (index) is the index from the highlighter object, and make a <styles> block to add to the <head> tag in the HTML.
Here's one way to do this:
let highlighter = [ { title: "Latin words", phrases: ["Lorem ipsum", "vestibulum nulla", "elit"], color: "green" }, { title: "Some other latin words", phrases: ["sit", "amet", "in", "nunc"], color: "#ba3030" }]; let highlighterStyles = "<style>"; for (let index in highlighter) { // We use `in` insead of `of` to get the indices const highlighterClass = `highlighter-${index}`; highlighterStyles += `.${highlighterClass} { background-color: ${highlighter[index].color}; } `; $("body").mark(highlighter[index].phrases, { className: highlighterClass, separateWordSearch: false }); } highlighterStyles += "</styles>"; $("head").append(highlighterStyles);
Result of the code above!
Result of the code above!
Now we have the core functionality of the script done. Let’s move on to some additional features and then extensionify what we have.
[1] Another way of doing this is to make every highlighted object be wrapped within a <span> with inline styles, like <span style="background-color:green">Lorem ipsum</span>, but inline styling is usually a bad practice and isn’t supported by mark.js.

Base Script: Styling & Trigger

Now we’ll add highlighting to a keypress handler instead of doing it automatically. We can use F6 since that’s not commonly used by other programs.
With a quick Stackoverflow search, we know out we can use jQuery’s keydown to track when F6 is pressed. Let’s functionalize the highlighting and then call this function when keydown is pressed.
$(window).keydown((e) => { if (e.keyCode == 117) { //F6 event.preventDefault(); highlightWords(); } });
Note: Be sure to click inside the JSFiddle HTML window before pressing F6, or otherwise it'll activate on the whole window instead of the frame.
OK, but we also want to unhighlight words when we press F6 instead of highlighting the same ones over and over and adding excess <style> blocks.
Tasks
1. In highlightWords(), give the <style> block an id of something like highlighter-styles. 2. Keep track of whether we've highlighted the window or not with a new boolean called highlighted. 3. If highlighted is false, do what we're doing already. 4. If highlighted is true, remove the <style> block with jQuery's $(selector).remove() and use mark.js's unmark() function.
Try to the above. When you’re ready here's a JSFiddle for up to this point!
Now let’s add some more styling, using a base class that’s attached to every phrase list.
Create a .highlighter class that's given to every phrase as well as the .highlighter-(index) one. For now, you can style this in the CSS section of JSFiddle, but in a Web Extension, we'd have to inject it into the browser, like what we're doing now.
.highlighter { border-radius: 0.3rem; padding: 0.1rem; color: white; font-weight: normal; box-shadow: inset 0 -0.1rem 0 rgba(20, 20, 20, 0.4); }
Here's some styling with a rounded border and box-shadow.
And add it to the mark call!
const highlighterBaseClass = "highlighter"; const highlighterClass = `${highlighterBaseClass}-${index}`; $("body").mark(highlighter[index].phrases, { className: `${highlighterBaseClass} ${highlighterClass}`, separateWordSearch: false });
Here's what the result looks like with the new styling!
Here's what the result looks like with the new styling!

And now we have a working highlighting MVP!

What’s left?

The rest of the work comes in extensionifying: allowing customization and adding the user interface. We want to let the user be able to pick the phrase lists and customize some settings like case sensitivity.
We add two more frameworks: WebExtensions and Bulma, explore their documentation, and then build out a UI and working chrome extension!
notion image
notion image

Bulma

  • Bulma is a CSS framework that helps us speed through building out the

WebExtensions

  • WebExtensions is a specification to write browser extensions compatible with Google Chrome, Firefox, and Opera. (Chrome Docs, Firefox Docs).

Eventually, we ended up with an extension like this:
notion image

Closing

I hope this was helpful!
Web extensions are great side project ideas since they’re useful, easily built, and cross-compatible with browsers. If you look at a lot of top extensions, many of them are pretty small like Highlighty and can be recreated in a few weekends.
With just this article, we’ve covered part of making an extension that has almost a greater feature set than one with 130,000 users. The tougher part is actually getting the user base and maintaining an app long-term.
To contribute to Highlighty: check out the list of issues and make pull requests on the GitHub.