Utilities
Utilities
Section titled “Utilities”Podcast Framework provides a comprehensive set of utility functions for common tasks like date formatting, text manipulation, and duration handling.
Import
Section titled “Import”import { formatDate, stripHTML, escapeHTML, decodeHTMLEntities, truncate, slugify, parseDuration, formatDuration} from '@rejected-media/podcast-framework-core';Date & Time Functions
Section titled “Date & Time Functions”formatDate()
Section titled “formatDate()”Format ISO date strings to human-readable format.
Signature:
function formatDate( dateString: string, locale?: string): stringParameters:
dateString- ISO date string or any valid date formatlocale- 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"}parseDuration()
Section titled “parseDuration()”Parse duration string to seconds.
Signature:
function parseDuration(duration: string): numberParameters:
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.}formatDuration()
Section titled “formatDuration()”Format seconds to readable duration string.
Signature:
function formatDuration(seconds: number): stringParameters:
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>Text Manipulation Functions
Section titled “Text Manipulation Functions”stripHTML()
Section titled “stripHTML()”Remove HTML tags and decode HTML entities.
Signature:
function stripHTML(html: string): stringParameters:
html- HTML string to strip
Returns: Plain text without HTML tags
Examples:
stripHTML('<p>Hello & 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>escapeHTML()
Section titled “escapeHTML()”Escape HTML to prevent XSS attacks.
Signature:
function escapeHTML(text: string): stringParameters:
text- Text to escape
Returns: HTML-safe text
Examples:
escapeHTML('<script>alert("xss")</script>')// → "<script>alert("xss")</script>"
escapeHTML("It's a test & demo")// → "It's a test & demo"
escapeHTML('Path: /home/user')// → "Path: /home/user"Usage:
// In email templatesconst html = ` <p>Name: ${escapeHTML(userInput.name)}</p> <p>Comment: ${escapeHTML(userInput.comment)}</p>`;Security:
// ❌ Vulnerable to XSSconst userInput = '<script>alert("xss")</script>';const html = `<p>${userInput}</p>`; // XSS!
// ✅ Safe from XSSconst html = `<p>${escapeHTML(userInput)}</p>`; // SafedecodeHTMLEntities()
Section titled “decodeHTMLEntities()”Decode HTML entities to their corresponding characters.
Signature:
function decodeHTMLEntities(text: string): stringParameters:
text- Text with HTML entities
Returns: Text with decoded entities
Examples:
decodeHTMLEntities('&') // → "&"decodeHTMLEntities('<') // → "<"decodeHTMLEntities('>') // → ">"decodeHTMLEntities('"') // → '"'decodeHTMLEntities(''') // → "'"decodeHTMLEntities(''') // → "'"decodeHTMLEntities('©') // → "©"decodeHTMLEntities(' ') // → " "Usage:
// Decode RSS feed contentconst rssDescription = '<p>Episode about AI & ML</p>';const decoded = decodeHTMLEntities(rssDescription);// → "<p>Episode about AI & ML</p>"truncate()
Section titled “truncate()”Truncate text to maximum length with ellipsis.
Signature:
function truncate( text: string, maxLength: number, suffix?: string): stringParameters:
text- Text to truncatemaxLength- Maximum length before truncationsuffix- 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} />slugify()
Section titled “slugify()”Convert text to URL-safe slug format.
Signature:
function slugify(text: string): stringParameters:
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 titleconst episodeTitle = 'The Future of Ethereum';const slug = slugify(episodeTitle);// → "the-future-of-ethereum"
// Use in URLconst url = `/episodes/${slug}`;Auto-generate slugs:
function createEpisode(title: string) { return { title, slug: slugify(title), // ... other fields };}Function Reference
Section titled “Function Reference”Summary Table
Section titled “Summary Table”| Function | Purpose | Input | Output |
|---|---|---|---|
formatDate() | Format dates | '2024-01-15' | "January 15, 2024" |
stripHTML() | Remove HTML | '<p>Text</p>' | "Text" |
escapeHTML() | Prevent XSS | '<script>' | "<script>" |
decodeHTMLEntities() | Decode entities | '&' | "&" |
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 time | 5025 | "1:23:45" |
Best Practices
Section titled “Best Practices”1. Always Escape User Input
Section titled “1. Always Escape User Input”// ❌ Vulnerableconst html = `<p>${userInput}</p>`;
// ✅ Safeconst html = `<p>${escapeHTML(userInput)}</p>`;2. Strip HTML for Meta Tags
Section titled “2. Strip HTML for Meta Tags”---const description = stripHTML(episode.description);---
<meta name="description" content={description} /><meta property="og:description" content={description} />3. Validate Before Parsing
Section titled “3. Validate Before Parsing”// ❌ May throw errorconst seconds = parseDuration(userInput);
// ✅ Validate firsttry { const seconds = parseDuration(userInput);} catch (error) { console.error('Invalid duration:', userInput); // Handle error}4. Use Consistent Slugs
Section titled “4. Use Consistent Slugs”// Generate slug once, use everywhereconst slug = slugify(title);
// URLconst url = `/episodes/${slug}`;
// Sanity documentconst doc = { slug: { current: slug, _type: 'slug' }};Common Patterns
Section titled “Common Patterns”Pattern 1: Episode Card
Section titled “Pattern 1: Episode Card”---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>Pattern 2: SEO Meta Tags
Section titled “Pattern 2: SEO Meta Tags”---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>Pattern 3: Duration Display
Section titled “Pattern 3: Duration Display”---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>Pattern 4: Safe User Content
Section titled “Pattern 4: Safe User Content”// In API routeimport { 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> `;};TypeScript Types
Section titled “TypeScript Types”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`;}Error Handling
Section titled “Error Handling”Date Functions
Section titled “Date Functions”// formatDate() throws on invalid inputtry { formatDate('not-a-date');} catch (error) { console.error(error.message); // → "Invalid date format: \"not-a-date\""}Duration Functions
Section titled “Duration Functions”// parseDuration() throws on invalid inputtry { 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"}Performance
Section titled “Performance”All utilities are optimized for performance:
| Function | Time Complexity | Notes |
|---|---|---|
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.1msstripHTML(500-char description): ~0.2msslugify('Episode Title'): ~0.3msparseDuration('1:23:45'): ~0.01msSecurity
Section titled “Security”XSS Prevention
Section titled “XSS Prevention”Always use escapeHTML() for user-generated content:
// ❌ Vulnerableconst html = `<div>${userInput}</div>`;
// ✅ Safeconst html = `<div>${escapeHTML(userInput)}</div>`;HTML Sanitization
Section titled “HTML Sanitization”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)} />Safe Slugs
Section titled “Safe Slugs”slugify() produces safe, predictable slugs:
slugify('../../../etc/passwd')// → "etc-passwd" (path traversal prevented)
slugify('<script>xss</script>')// → "scriptxssscript" (tags removed)Locale Support
Section titled “Locale Support”Supported Locales
Section titled “Supported Locales”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') // → "١٥ يناير ٢٠٢٤"Custom Format Options
Section titled “Custom Format Options”For more control, use native toLocaleDateString:
const date = new Date(episode.publishDate);
// Short formatdate.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric'});// → "Jan 15, 2024"
// Long formatdate.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'});// → "Monday, January 15, 2024"Testing
Section titled “Testing”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');});Related
Section titled “Related”- Sanity Helpers - Higher-level data fetching
- Server Services - Use utilities in services
Next Steps
Section titled “Next Steps”- Theme System - Theming utilities
- Sanity Helpers - Data fetching helpers
- Hosting Adapter - Platform abstraction