import { discoverBlocks, mergeHeadingIdsIntoContentSegments, type ContentSegment, } from "@/lib/blog-blocks"; export type { ContentSegment } from "@/lib/blog-blocks"; /** * Creates a URL-safe slug from heading text (strip HTML, normalize, Persian-friendly). */ function slugify(text: string): string { const stripped = text.replace(/<[^>]*>/g, "").trim(); return stripped .replace(/\s+/g, "-") .replace(/[^\p{L}\p{N}-]/gu, "") .replace(/-+/g, "-") .replace(/^-|-$/g, "") .toLowerCase() || "section"; } export interface TocItem { id: string; text: string; level: number; // 2 = h2, 3 = h3, 4 = h4 } const HEADING_REGEX = /<(h[234])[^>]*>([\s\S]*?)<\/\1>/gi; const usedIds = new Map(); function ensureUniqueId(baseId: string): string { const count = usedIds.get(baseId) ?? 0; usedIds.set(baseId, count + 1); return count === 0 ? baseId : `${baseId}-${count}`; } /** * پارس محتوا: ابتدا بلاک‌ها کشف و به JSON تبدیل می‌شوند (blog-blocks)، * سپس id سرنویس‌ها تزریق می‌شود و سگمنت‌های نهایی برای رندر برمی‌گردند. */ export function parseContentHeadings(html: string): { contentWithIds: string; toc: TocItem[]; segments: ContentSegment[]; } { const { htmlWithPlaceholders, segments: rawSegments } = discoverBlocks(html); usedIds.clear(); const toc: TocItem[] = []; let lastIndex = 0; let match: RegExpExecArray | null; const parts: string[] = []; const regex = new RegExp(HEADING_REGEX.source, "gi"); while ((match = regex.exec(htmlWithPlaceholders)) !== null) { const [fullMatch, tag, inner] = match; const level = parseInt(tag.charAt(1), 10) as 2 | 3 | 4; const text = inner.replace(/<[^>]*>/g, "").trim(); const baseId = slugify(text); const id = ensureUniqueId(baseId); toc.push({ id, text, level }); const openTag = fullMatch.slice(0, fullMatch.indexOf(">") + 1); const hasId = /^\s*]*\bid\s*=/i.test(openTag); const newOpenTag = hasId ? openTag : openTag.replace(/^(<\s*h[234])(\s|>)/i, `$1 id="${id}"$2`); parts.push(htmlWithPlaceholders.slice(lastIndex, match.index)); parts.push(newOpenTag + inner + ``); lastIndex = regex.lastIndex; } parts.push(htmlWithPlaceholders.slice(lastIndex)); const contentWithIds = parts.join(""); const segments = mergeHeadingIdsIntoContentSegments( contentWithIds, rawSegments ); return { contentWithIds, toc, segments }; } /** Average reading speed in words per minute (Persian/English mix). */ const WPM = 200; /** * Estimates reading time in minutes from HTML content. */ export function getReadingTimeMinutes(html: string): number { const text = html.replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim(); const wordCount = text.split(/\s+/).filter(Boolean).length; return Math.max(1, Math.ceil(wordCount / WPM)); }