Skip to content

Utilities

Podcast Framework provides a comprehensive set of utility functions for common tasks like date formatting, text manipulation, and duration handling.

import {
formatDate,
stripHTML,
escapeHTML,
decodeHTMLEntities,
truncate,
slugify,
parseDuration,
formatDuration
} from '@rejected-media/podcast-framework-core';

Format ISO date strings to human-readable format.

Signature:

function formatDate(
dateString: string,
locale?: string
): string

Parameters:

  • dateString - ISO date string or any valid date format
  • locale - Locale for formatting (default: 'en-US')

Returns: Formatted date string

Examples:

formatDate('2024-01-15')
// → "January 15, 2024"
formatDate('2024-01-15', 'es-ES')
// → "15 de enero de 2024"
formatDate('2024-12-25', 'fr-FR')
// → "25 décembre 2024"

Usage in Templates:

---
import { getEpisodes, formatDate } from '@rejected-media/podcast-framework-core';
const episodes = await getEpisodes();
---
{episodes.map(episode => (
<article>
<h2>{episode.title}</h2>
<p>Published: {formatDate(episode.publishDate)}</p>
</article>
))}

Error Handling:

try {
formatDate('invalid-date');
} catch (error) {
// Error: Invalid date format: "invalid-date"
}

Parse duration string to seconds.

Signature:

function parseDuration(duration: string): number

Parameters:

  • duration - Duration string in HH:MM:SS, MM:SS, or SS format

Returns: Duration in seconds

Examples:

parseDuration('1:23:45') // → 5025 (1 hour, 23 minutes, 45 seconds)
parseDuration('23:45') // → 1425 (23 minutes, 45 seconds)
parseDuration('45') // → 45 (45 seconds)

Usage:

const durationInSeconds = parseDuration(episode.duration);
if (durationInSeconds > 3600) {
console.log('Long episode (over 1 hour)');
}

Error Handling:

try {
parseDuration('invalid');
} catch (error) {
// Error: Invalid duration format: "invalid"
}
try {
parseDuration('1:2:3:4');
} catch (error) {
// Error: Invalid duration format: "1:2:3:4". Too many colons.
}

Format seconds to readable duration string.

Signature:

function formatDuration(seconds: number): string

Parameters:

  • seconds - Duration in seconds

Returns: Formatted duration string

Examples:

formatDuration(5025) // → "1:23:45"
formatDuration(1425) // → "23:45"
formatDuration(45) // → "0:45"
formatDuration(3661) // → "1:01:01"

Usage:

---
const durationSeconds = parseDuration(episode.duration);
const formatted = formatDuration(durationSeconds);
---
<p>Duration: {formatted}</p>

Remove HTML tags and decode HTML entities.

Signature:

function stripHTML(html: string): string

Parameters:

  • html - HTML string to strip

Returns: Plain text without HTML tags

Examples:

stripHTML('<p>Hello &amp; welcome</p>')
// → "Hello & welcome"
stripHTML('<strong>Bold text</strong> and <em>italic</em>')
// → "Bold text and italic"
stripHTML('<script>alert("xss")</script>')
// → 'alert("xss")' (tags removed, safe)

Usage:

---
import { stripHTML } from '@rejected-media/podcast-framework-core';
---
<meta
name="description"
content={stripHTML(episode.description)}
/>
<p class="line-clamp-3">
{stripHTML(episode.description)}
</p>

Escape HTML to prevent XSS attacks.

Signature:

function escapeHTML(text: string): string

Parameters:

  • text - Text to escape

Returns: HTML-safe text

Examples:

escapeHTML('<script>alert("xss")</script>')
// → "&lt;script&gt;alert(&quot;xss&quot;)&lt;/script&gt;"
escapeHTML("It's a test & demo")
// → "It&#39;s a test &amp; demo"
escapeHTML('Path: /home/user')
// → "Path: &#x2F;home&#x2F;user"

Usage:

// In email templates
const html = `
<p>Name: ${escapeHTML(userInput.name)}</p>
<p>Comment: ${escapeHTML(userInput.comment)}</p>
`;

Security:

// ❌ Vulnerable to XSS
const userInput = '<script>alert("xss")</script>';
const html = `<p>${userInput}</p>`; // XSS!
// ✅ Safe from XSS
const html = `<p>${escapeHTML(userInput)}</p>`; // Safe

Decode HTML entities to their corresponding characters.

Signature:

function decodeHTMLEntities(text: string): string

Parameters:

  • text - Text with HTML entities

Returns: Text with decoded entities

Examples:

decodeHTMLEntities('&amp;') // → "&"
decodeHTMLEntities('&lt;') // → "<"
decodeHTMLEntities('&gt;') // → ">"
decodeHTMLEntities('&quot;') // → '"'
decodeHTMLEntities('&#39;') // → "'"
decodeHTMLEntities('&#x27;') // → "'"
decodeHTMLEntities('&copy;') // → "©"
decodeHTMLEntities('&nbsp;') // → " "

Usage:

// Decode RSS feed content
const rssDescription = '&lt;p&gt;Episode about AI &amp; ML&lt;/p&gt;';
const decoded = decodeHTMLEntities(rssDescription);
// → "<p>Episode about AI & ML</p>"

Truncate text to maximum length with ellipsis.

Signature:

function truncate(
text: string,
maxLength: number,
suffix?: string
): string

Parameters:

  • text - Text to truncate
  • maxLength - Maximum length before truncation
  • suffix - Suffix to add when truncated (default: '...')

Returns: Truncated text

Examples:

truncate('This is a long text', 10)
// → "This is..."
truncate('Short', 10)
// → "Short" (unchanged)
truncate('Custom suffix example', 10, '')
// → "Custom su→"
truncate('Exact length', 12)
// → "Exact length" (no truncation at exact length)

Usage:

---
import { truncate } from '@rejected-media/podcast-framework-core';
---
<div class="episode-card">
<h3>{episode.title}</h3>
<p>{truncate(episode.description, 150)}</p>
</div>

SEO:

---
const metaDescription = truncate(
stripHTML(episode.description),
160 // Google's meta description limit
);
---
<meta name="description" content={metaDescription} />

Convert text to URL-safe slug format.

Signature:

function slugify(text: string): string

Parameters:

  • text - Text to slugify

Returns: URL-safe slug

Examples:

slugify('Hello World!')
// → "hello-world"
slugify('Episode #42: AI & ML')
// → "episode-42-ai-ml"
slugify(' Spaces and Special!@#$% ')
// → "spaces-and-special"
slugify('François René de Château')
// → "francois-rene-de-chateau"

Usage:

// Generate slug from title
const episodeTitle = 'The Future of Ethereum';
const slug = slugify(episodeTitle);
// → "the-future-of-ethereum"
// Use in URL
const url = `/episodes/${slug}`;

Auto-generate slugs:

function createEpisode(title: string) {
return {
title,
slug: slugify(title),
// ... other fields
};
}
FunctionPurposeInputOutput
formatDate()Format dates'2024-01-15'"January 15, 2024"
stripHTML()Remove HTML'<p>Text</p>'"Text"
escapeHTML()Prevent XSS'<script>'"&lt;script&gt;"
decodeHTMLEntities()Decode entities'&amp;'"&"
truncate()Limit length'Long text', 10"Long te..."
slugify()URL-safe slugs'Hello World'"hello-world"
parseDuration()Parse time'1:23:45'5025
formatDuration()Format time5025"1:23:45"
// ❌ Vulnerable
const html = `<p>${userInput}</p>`;
// ✅ Safe
const html = `<p>${escapeHTML(userInput)}</p>`;
---
const description = stripHTML(episode.description);
---
<meta name="description" content={description} />
<meta property="og:description" content={description} />
// ❌ May throw error
const seconds = parseDuration(userInput);
// ✅ Validate first
try {
const seconds = parseDuration(userInput);
} catch (error) {
console.error('Invalid duration:', userInput);
// Handle error
}
// Generate slug once, use everywhere
const slug = slugify(title);
// URL
const url = `/episodes/${slug}`;
// Sanity document
const doc = {
slug: { current: slug, _type: 'slug' }
};
---
import { formatDate, truncate, stripHTML } from '@rejected-media/podcast-framework-core';
---
<article>
<h2>{episode.title}</h2>
<p class="date">{formatDate(episode.publishDate)}</p>
<p class="description">
{truncate(stripHTML(episode.description), 150)}
</p>
<a href={`/episodes/${episode.slug.current}`}>
Read more →
</a>
</article>
---
import { formatDate, stripHTML, truncate } from '@rejected-media/podcast-framework-core';
const title = episode.title;
const description = truncate(stripHTML(episode.description), 160);
const publishDate = formatDate(episode.publishDate, 'en-US');
---
<head>
<title>{title}</title>
<meta name="description" content={description} />
<meta property="og:title" content={title} />
<meta property="og:description" content={description} />
<meta property="article:published_time" content={episode.publishDate} />
</head>
---
import { parseDuration, formatDuration } from '@rejected-media/podcast-framework-core';
const seconds = parseDuration(episode.duration);
const formatted = formatDuration(seconds);
const hours = Math.floor(seconds / 3600);
---
<div class="duration">
<span>{formatted}</span>
{hours > 0 && <span class="badge">Long episode</span>}
</div>
// In API route
import { escapeHTML } from '@rejected-media/podcast-framework-core';
export const POST: APIRoute = async ({ request }) => {
const data = await request.json();
// Escape all user input
const contribution = {
topic: escapeHTML(data.topic),
description: escapeHTML(data.description),
submitterName: escapeHTML(data.name)
};
// Safe to use in email/HTML
const emailHTML = `
<h3>${contribution.topic}</h3>
<p>${contribution.description}</p>
<p>From: ${contribution.submitterName}</p>
`;
};

All utilities are fully typed:

import type { Episode } from '@rejected-media/podcast-framework-core';
function processEpisode(episode: Episode): string {
// TypeScript knows the shape of episode
const date = formatDate(episode.publishDate); // ✅ Type-safe
const slug = slugify(episode.title); // ✅ Type-safe
const seconds = parseDuration(episode.duration); // ✅ Type-safe
return `${date} - ${slug} - ${seconds}s`;
}
// formatDate() throws on invalid input
try {
formatDate('not-a-date');
} catch (error) {
console.error(error.message);
// → "Invalid date format: \"not-a-date\""
}
// parseDuration() throws on invalid input
try {
parseDuration('1:2:3:4'); // Too many colons
} catch (error) {
console.error(error.message);
// → "Invalid duration format: \"1:2:3:4\". Too many colons."
}
try {
parseDuration('abc:def'); // Not numbers
} catch (error) {
console.error(error.message);
// → "Invalid duration format: \"abc:def\". Expected HH:MM:SS, MM:SS, or SS"
}

All utilities are optimized for performance:

FunctionTime ComplexityNotes
formatDate()O(1)Uses native Date.toLocaleDateString()
stripHTML()O(n)Single regex pass
escapeHTML()O(n)Single pass with lookup
decodeHTMLEntities()O(n)Three regex passes
truncate()O(1)Substring operation
slugify()O(n)Multiple regex passes
parseDuration()O(1)Simple arithmetic
formatDuration()O(1)Simple arithmetic

Benchmarks (typical episode data):

formatDate('2024-01-15'): ~0.1ms
stripHTML(500-char description): ~0.2ms
slugify('Episode Title'): ~0.3ms
parseDuration('1:23:45'): ~0.01ms

Always use escapeHTML() for user-generated content:

// ❌ Vulnerable
const html = `<div>${userInput}</div>`;
// ✅ Safe
const html = `<div>${escapeHTML(userInput)}</div>`;

stripHTML() removes all HTML tags:

const userInput = '<script>alert("xss")</script><p>Safe text</p>';
const safe = stripHTML(userInput);
// → 'alert("xss")Safe text'
// For meta tags (no HTML allowed)
<meta name="description" content={stripHTML(description)} />

slugify() produces safe, predictable slugs:

slugify('../../../etc/passwd')
// → "etc-passwd" (path traversal prevented)
slugify('<script>xss</script>')
// → "scriptxssscript" (tags removed)

formatDate() supports all locales via Intl.DateTimeFormat:

formatDate('2024-01-15', 'en-US') // → "January 15, 2024"
formatDate('2024-01-15', 'en-GB') // → "15 January 2024"
formatDate('2024-01-15', 'de-DE') // → "15. Januar 2024"
formatDate('2024-01-15', 'ja-JP') // → "2024年1月15日"
formatDate('2024-01-15', 'ar-EG') // → "١٥ يناير ٢٠٢٤"

For more control, use native toLocaleDateString:

const date = new Date(episode.publishDate);
// Short format
date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric'
});
// → "Jan 15, 2024"
// Long format
date.toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
// → "Monday, January 15, 2024"

All utilities have comprehensive test coverage:

import { test, expect } from 'vitest';
import { slugify } from '@rejected-media/podcast-framework-core';
test('slugify converts to lowercase', () => {
expect(slugify('HELLO WORLD')).toBe('hello-world');
});
test('slugify removes special characters', () => {
expect(slugify('Hello@#$%World!')).toBe('helloworld');
});
test('slugify handles multiple spaces', () => {
expect(slugify('hello world')).toBe('hello-world');
});