Building a Simple Read Time Calculator in JS/TS
I've been tinkering as usual and thought I'd share a cool little script I created—a read-time calculator that works with HTML and Markdown. Do you know those "2 min read" labels you see on blog posts? Let's go over how to build one from scratch.
Why a Read Time Calculator?
Well, I'm a sucker for those reading time estimates. They help me decide if I have time to dive into an article during my coffee break or save it for later. So, I figured, why not build one myself?
I've seen packages that add this feature, but when I can, I like to avoid adding a package for something I thought would be simple enough to solve.
Counting words and doing some simple math seemed easy, but cleaning was the challenge.
Defining Types and Constants
Let's start by defining our content types and a default reading speed:
type ContentType = 'html' | 'markdown'; const WORDS_PER_MINUTE = 200;
Creating Helper Functions
We'll need a few helper functions to do the heavy lifting:
const countWords = (text: string): number => text.trim().split(/\s+/).length; const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, ''); const stripMarkdown = (markdown: string): string => { let cleaned = markdown; // Remove headers cleaned = cleaned.replace(/#{1,6}\s?/g, ''); // Remove emphasis cleaned = cleaned.replace(/(\*\*|__)(.*?)\1/g, '$2'); cleaned = cleaned.replace(/(\*|_)(.*?)\1/g, '$2'); // Remove links cleaned = cleaned.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); // Remove code blocks cleaned = cleaned.replace(/`{3}[\s\S]*?`{3}/g, ''); // Remove inline code cleaned = cleaned.replace(/`(.+?)`/g, '$1'); // Remove images cleaned = cleaned.replace(/!\[([^\]]+)\]\([^\)]+\)/g, ''); return cleaned; };
These functions will count words, strip HTML tags, and remove Markdown formatting.
The Cleaning Function
Now, let's create a function that chooses between stripping HTML or Markdown:
const cleanContent = (content: string, contentType: ContentType): string => contentType === 'html' ? stripHtml(content) : stripMarkdown(content);
Here's where it all comes together:
const calculateReadTime = ( content: string, contentType: ContentType = 'markdown', wordsPerMinute: number = WORDS_PER_MINUTE ): string => { const cleaned = cleanContent(content, contentType); const wordCount = countWords(cleaned); const minutes = Math.ceil(wordCount / wordsPerMinute); return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`; };
This function takes the content, cleans it, counts the words, and calculates the reading time.
Let's Test It Out!
Time to put our calculator to work:
const htmlContent = "<p>This is some <strong>HTML</strong> content.</p>"; const markdownContent = "# This is a header\n\nThis is some **Markdown** content."; console.log(`HTML read time: ${calculateReadTime(htmlContent, 'html')} minutes`); console.log(`Markdown read time: ${calculateReadTime(markdownContent, 'markdown')} minutes`); // Let's try with different reading speeds console.log(`HTML read time (300 wpm): ${calculateReadTime(htmlContent, 'html', 300)} minutes`); console.log(`Markdown read time (150 wpm): ${calculateReadTime(markdownContent, 'markdown', 150)} minutes`);
And just like that, you should have a pretty decent read-time estimate.
Here's the full snippet to steal:
type ContentType = 'html' | 'markdown'; const WORDS_PER_MINUTE = 200; const countWords = (text: string): number => text.trim().split(/\s+/).length; const stripHtml = (html: string): string => html.replace(/<[^>]*>/g, ''); const stripMarkdown = (markdown: string): string => { let cleaned = markdown; // Remove headers cleaned = cleaned.replace(/#{1,6}\s?/g, ''); // Remove emphasis cleaned = cleaned.replace(/(\*\*|__)(.*?)\1/g, '$2'); cleaned = cleaned.replace(/(\*|_)(.*?)\1/g, '$2'); // Remove links cleaned = cleaned.replace(/\[([^\]]+)\]\([^\)]+\)/g, '$1'); // Remove code blocks cleaned = cleaned.replace(/`{3}[\s\S]*?`{3}/g, ''); // Remove inline code cleaned = cleaned.replace(/`(.+?)`/g, '$1'); // Remove images cleaned = cleaned.replace(/!\[([^\]]+)\]\([^\)]+\)/g, ''); return cleaned; }; const cleanContent = (content: string, contentType: ContentType): string => contentType === 'html' ? stripHtml(content) : stripMarkdown(content); const calculateReadTime = ( content: string, contentType: ContentType = 'markdown', wordsPerMinute: number = WORDS_PER_MINUTE ): string => { const cleaned = cleanContent(content, contentType); const wordCount = countWords(cleaned); const minutes = Math.ceil(wordCount / wordsPerMinute); return `${minutes} ${minutes === 1 ? 'minute' : 'minutes'}`; }; // Example usage const htmlContent = "<p>This is some <strong>HTML</strong> content.</p>"; const markdownContent = "# This is a header\n\nThis is some **Markdown** content."; console.log(`HTML read time: ${calculateReadTime(htmlContent, 'html')}`); console.log(`Markdown read time: ${calculateReadTime(markdownContent, 'markdown')}`); // Additional examples with custom words per minute console.log(`HTML read time (300 wpm): ${calculateReadTime(htmlContent, 'html', 300)}`); console.log(`Markdown read time (150 wpm): ${calculateReadTime(markdownContent, 'markdown', 150)}`); // Example to demonstrate singular form const shortContent = "This is a very short sentence."; console.log(`Short content read time: ${calculateReadTime(shortContent, 'markdown', 5)}`);