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)}`);
TypeScriptJavaScript
Avatar for Niall Maher

Written by Niall Maher

Founder of Codú - The web developer community! I've worked in nearly every corner of technology businesses: Lead Developer, Software Architect, Product Manager, CTO, and now happily a Founder.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.