blog. feed tags

Firefox word count addon

· read · project

Start Here

In a classic case of not-invented-here-itis, I wrote my own Firefox addon to count how many words are selected. It’s available on addons.mozilla.org. For example, I can see that this page has five hundred words by pressing Ctrl + A and right-clicking.

Here’s all the source code, which I’m releasing under the MIT license. Feel free to copy-paste this into your own addon if you don’t trust me, and feel free to further upload it yourself (but please do it under a different name) if you don’t want to go to “Debug Add-ons” and load it temporarily every time you open Firefox.

manifest.json

{
	"manifest_version": 2,
	"name": "__MSG_extensionName__",
	"description": "__MSG_extensionDescription__",
	"default_locale": "en",
	"version": "1.1",
	"background": {
		"scripts": ["background.js"]
	},
	"icons": {
		"48": "icon/icon-plain.svg",
		"96": "icon/icon-plain.svg"
	},
	"permissions": [
		"contextMenus",
		"<all_urls>"
	]
}

background.js

const WORDCOUNT_CONTEXT_MENU_ID = "word-count"

browser.contextMenus.create({
	id: WORDCOUNT_CONTEXT_MENU_ID,
	title: browser.i18n.getMessage("contextMenuItemWordCountPlural", "unknown"),
	contexts: ["selection"],

	visible: false,
})

function count_words(text) {
	let regex = /(\p{Alphabetic}(\p{Quotation_Mark}\p{Alphabetic}+)*)+/gu

	let count = 0
	while (regex.exec(text) != null) count++

	return count
}

browser.contextMenus.onShown.addListener(async (info) => {
	if (info.selectionText == null) {
		browser.contextMenus.update(WORDCOUNT_CONTEXT_MENU_ID, {
			visible: false,
		})

		browser.contextMenus.refresh()
		return
	}

	let count = count_words(info.selectionText)

	/* Only english; this'll have to be changed if proper localizations are ever added. */
	const plurality = count == 1 ? "Singular" : "Plural"

	let truncated = false;

	/* Firefox truncates `info.selectionText` to 16384 characters. As a complete ballpark I chose to limit `count` to `2000` as a round number placeholder. */
	if (count > 2000 || info.selectionText.length == 16384) {
		count = 2000;
		truncated = true;
	}

	let count_string = count.toLocaleString()

	let title = truncated
		? browser.i18n.getMessage("contextMenuItemWordCountTruncated", count_string)
		: browser.i18n.getMessage(`contextMenuItemWordCount${plurality}`, count_string)

	/* Show the menu every time, otherwise it moves up and down a bit; it's something to do with addon callback order or menu order, where any items that are set to visible after the menu's opened go to the end every time, but if the menu's re-opened they show in addon order? Not sure. */
	browser.contextMenus.update(WORDCOUNT_CONTEXT_MENU_ID, {
		title,
		visible: true,
	})

	browser.contextMenus.refresh()
})

browser.contextMenus.onHidden.addListener(async (_info) => {
	browser.contextMenus.update(WORDCOUNT_CONTEXT_MENU_ID, {
		visible: false,
	})

	browser.contextMenus.refresh()
})

_locales/en/messages.json

{
	"extensionName": {
		"message": "Very Small Word Counter",
		"description": "Extension name"
	},
	"extensionDescription": {
		"message": "Easily see how many words are selected, right from the context menu.",
		"description": "Extension description"
	},
	"contextMenuItemWordCountSingular": {
		"message": "$1 word",
		"description": "Word count number; used when the selection has exactly one word"
	},
	"contextMenuItemWordCountPlural": {
		"message": "$1 words",
		"description": "Word count number; used when the selection has an exact number of words, and not shown when there is exactly one word"
	},
	"contextMenuItemWordCountTruncated": {
		"message": "$1+ words",
		"description": "Word count number; used when the selection has been truncated and the exact word count is not known"
	}
}