Skip to main content
Glama
northernvariables

FedMCP - Federal Parliamentary Information

mentionResolver.ts9.7 kB
/** * Mention Resolver - Resolve @mentions to URLs and metadata * * Converts parsed mentions into navigable URLs and display information. */ import type { ParsedMention, MentionType } from './mentionParser'; /** * Resolved mention with navigation and display info */ export interface ResolvedMention extends ParsedMention { /** URL to navigate to */ url: string; /** Display label for the mention */ label: string; /** Whether this mention can have discussions */ hasDiscussions: boolean; /** CSS class for styling based on type */ colorClass: string; } /** * Configuration for different locales */ interface LocaleConfig { bills: string; mps: string; committees: string; votes: string; debates: string; petitions: string; } const LOCALE_PATHS: Record<string, LocaleConfig> = { en: { bills: 'bills', mps: 'mps', committees: 'committees', votes: 'votes', debates: 'debates', petitions: 'petitions', }, fr: { bills: 'projets-de-loi', mps: 'deputes', committees: 'comites', votes: 'votes', debates: 'debats', petitions: 'petitions', }, }; /** * Color classes for each entity type */ const TYPE_COLORS: Record<MentionType, string> = { bill: 'text-blue-600 bg-blue-100 dark:text-blue-400 dark:bg-blue-900/30', mp: 'text-green-600 bg-green-100 dark:text-green-400 dark:bg-green-900/30', committee: 'text-purple-600 bg-purple-100 dark:text-purple-400 dark:bg-purple-900/30', vote: 'text-orange-600 bg-orange-100 dark:text-orange-400 dark:bg-orange-900/30', debate: 'text-red-600 bg-red-100 dark:text-red-400 dark:bg-red-900/30', petition: 'text-teal-600 bg-teal-100 dark:text-teal-400 dark:bg-teal-900/30', }; /** * Entity types that support discussions (Phase 1A: only bills) */ const DISCUSSION_ENABLED_TYPES: MentionType[] = ['bill']; /** * Resolve a bill mention to URL and metadata */ function resolveBillMention( mention: ParsedMention, locale: string, session: string = '45-1' ): Partial<ResolvedMention> { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; const billNumber = mention.id.toLowerCase(); // Build URL with optional section anchor let url = `/${locale}/${paths.bills}/${session}/${billNumber}`; if (mention.subId) { url += `#${mention.subId}`; } // Build label let label = `Bill ${mention.id.toUpperCase()}`; if (mention.subId) { // Format section reference (e.g., "s2.1.a" -> "Section 2.1.a") const sectionRef = mention.subId; if (sectionRef.startsWith('s')) { label += ` Section ${sectionRef.slice(1)}`; } else if (sectionRef.startsWith('part-')) { label += ` Part ${sectionRef.replace('part-', '')}`; } else { label += ` ${sectionRef}`; } } return { url, label }; } /** * Resolve an MP mention to URL and metadata */ function resolveMpMention( mention: ParsedMention, locale: string ): Partial<ResolvedMention> { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; // Convert slug to display name const label = mention.id .split('-') .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join(' '); return { url: `/${locale}/${paths.mps}/${mention.id}`, label, }; } /** * Resolve a committee mention to URL and metadata */ function resolveCommitteeMention( mention: ParsedMention, locale: string ): Partial<ResolvedMention> { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; const code = mention.id.toUpperCase(); let url = `/${locale}/${paths.committees}/${code.toLowerCase()}`; let label = code; if (mention.subId) { url += `/meetings/${mention.subId}`; label += ` Meeting #${mention.subId}`; } return { url, label }; } /** * Resolve a vote mention to URL and metadata */ function resolveVoteMention( mention: ParsedMention, locale: string ): Partial<ResolvedMention> { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; const [session, voteNumber] = [mention.id, mention.subId]; return { url: `/${locale}/${paths.votes}/${session}/${voteNumber}`, label: locale === 'fr' ? `Vote #${voteNumber}` : `Vote #${voteNumber}`, }; } /** * Resolve a debate mention to URL and metadata */ function resolveDebateMention( mention: ParsedMention, locale: string ): Partial<ResolvedMention> { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; let url = `/${locale}/${paths.debates}/${mention.id}`; let label = mention.id; if (mention.subId) { // Add time anchor const time = mention.subId.replace(':', '-'); url += `#${time}`; label += ` at ${mention.subId}`; } return { url, label }; } /** * Resolve a petition mention to URL and metadata */ function resolvePetitionMention( mention: ParsedMention, locale: string ): Partial<ResolvedMention> { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; return { url: `/${locale}/${paths.petitions}/${mention.id}`, label: `Petition ${mention.id.toUpperCase()}`, }; } /** * Resolve a parsed mention to full URL and metadata * * @param mention - Parsed mention object * @param locale - Current locale (en/fr) * @param options - Additional options (e.g., default session) * @returns Resolved mention with URL and display info */ export function resolveMention( mention: ParsedMention, locale: string = 'en', options: { session?: string } = {} ): ResolvedMention { let resolved: Partial<ResolvedMention> = {}; switch (mention.type) { case 'bill': resolved = resolveBillMention(mention, locale, options.session); break; case 'mp': resolved = resolveMpMention(mention, locale); break; case 'committee': resolved = resolveCommitteeMention(mention, locale); break; case 'vote': resolved = resolveVoteMention(mention, locale); break; case 'debate': resolved = resolveDebateMention(mention, locale); break; case 'petition': resolved = resolvePetitionMention(mention, locale); break; } return { ...mention, url: resolved.url || '#', label: resolved.label || mention.raw, hasDiscussions: DISCUSSION_ENABLED_TYPES.includes(mention.type), colorClass: TYPE_COLORS[mention.type], }; } /** * Resolve multiple mentions from text * * @param mentions - Array of parsed mentions * @param locale - Current locale * @param options - Additional options * @returns Array of resolved mentions */ export function resolveMentions( mentions: ParsedMention[], locale: string = 'en', options: { session?: string } = {} ): ResolvedMention[] { return mentions.map((mention) => resolveMention(mention, locale, options)); } /** * Convert text with mentions to React elements * This is a simplified version - in production you'd use a proper renderer * * @param text - Text containing mentions * @param mentions - Resolved mentions * @returns Array of text segments and mention objects */ export function segmentTextWithMentions( text: string, mentions: ResolvedMention[] ): Array<{ type: 'text'; content: string } | { type: 'mention'; mention: ResolvedMention }> { if (mentions.length === 0) { return [{ type: 'text', content: text }]; } const segments: Array< { type: 'text'; content: string } | { type: 'mention'; mention: ResolvedMention } > = []; let lastIndex = 0; for (const mention of mentions) { // Add text before this mention if (mention.startIndex > lastIndex) { segments.push({ type: 'text', content: text.slice(lastIndex, mention.startIndex), }); } // Add the mention segments.push({ type: 'mention', mention, }); lastIndex = mention.endIndex; } // Add remaining text after last mention if (lastIndex < text.length) { segments.push({ type: 'text', content: text.slice(lastIndex), }); } return segments; } /** * Build a mention URL from components (without parsing) * * @param type - Entity type * @param id - Primary identifier * @param locale - Current locale * @param options - Additional options * @returns URL string */ export function buildMentionUrl( type: MentionType, id: string, locale: string = 'en', options: { session?: string; subId?: string } = {} ): string { const paths = LOCALE_PATHS[locale] || LOCALE_PATHS.en; switch (type) { case 'bill': const session = options.session || '45-1'; let billUrl = `/${locale}/${paths.bills}/${session}/${id.toLowerCase()}`; if (options.subId) { billUrl += `#${options.subId}`; } return billUrl; case 'mp': return `/${locale}/${paths.mps}/${id}`; case 'committee': let committeeUrl = `/${locale}/${paths.committees}/${id.toLowerCase()}`; if (options.subId) { committeeUrl += `/meetings/${options.subId}`; } return committeeUrl; case 'vote': return `/${locale}/${paths.votes}/${options.session || '45-1'}/${id}`; case 'debate': let debateUrl = `/${locale}/${paths.debates}/${id}`; if (options.subId) { debateUrl += `#${options.subId.replace(':', '-')}`; } return debateUrl; case 'petition': return `/${locale}/${paths.petitions}/${id.toLowerCase()}`; default: return '#'; } } /** * Check if an entity type supports discussions * * @param type - Entity type * @returns True if discussions are enabled for this type */ export function canHaveDiscussions(type: MentionType): boolean { return DISCUSSION_ENABLED_TYPES.includes(type); } export default { resolveMention, resolveMentions, segmentTextWithMentions, buildMentionUrl, canHaveDiscussions, };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/northernvariables/FedMCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server