apple-hig-mcp
Version:
High-performance MCP server providing instant access to Apple's Human Interface Guidelines via hybrid static/dynamic content delivery
360 lines • 14.7 kB
JavaScript
/**
* HIGContentExtractor
*
* Specialized service for extracting high-quality content from Apple's HIG pages
* with advanced quality scoring and validation to achieve 95%+ real content SLA.
*/
export class HIGContentExtractor {
appleDesignTerms = [
'accessibility', 'animation', 'branding', 'buttons', 'color', 'controls',
'design', 'feedback', 'gestures', 'haptics', 'icons', 'images', 'input',
'interface', 'layout', 'materials', 'motion', 'navigation', 'presentation',
'selection', 'status', 'system', 'typography', 'visual', 'widgets',
'human interface guidelines', 'user experience', 'user interface',
'touch target', 'dynamic type', 'voiceover', 'dark mode', 'light mode'
];
applePlatformTerms = [
'ios', 'iphone', 'ipad', 'macos', 'mac', 'imac', 'macbook',
'watchos', 'apple watch', 'tvos', 'apple tv', 'visionos', 'vision pro',
'swiftui', 'uikit', 'appkit', 'core animation', 'core graphics'
];
fallbackIndicators = [
'this page requires javascript',
'single page application',
'please turn on javascript',
'javascript is required',
'content not available',
'loading...',
'page not found'
];
/**
* Extract and process content from raw HTML or text
*/
async extractContent(rawContent, section) {
if (process.env.NODE_ENV === 'development') {
console.log(`[HIGContentExtractor] Processing content for: ${section.title}`);
}
// Clean and normalize the content
const cleanedContent = this.cleanRawContent(rawContent);
// Extract structured information
const codeExamples = this.extractCodeExamples(cleanedContent);
const imageReferences = this.extractImageReferences(cleanedContent);
const headings = this.extractHeadings(cleanedContent);
const relatedSections = this.extractRelatedSections(cleanedContent);
const keywords = this.extractKeywords(cleanedContent, section);
// Generate table of contents
const tableOfContents = this.generateTableOfContents(headings);
// Generate summary
const summary = this.generateSummary(cleanedContent, section);
// Calculate quality metrics
const quality = this.calculateQualityMetrics(cleanedContent, {
codeExamplesCount: codeExamples.length,
imageReferencesCount: imageReferences.length,
headingCount: headings.length
});
if (process.env.NODE_ENV === 'development') {
console.log(`[HIGContentExtractor] Content quality score: ${quality.score.toFixed(2)} for ${section.title}`);
}
return {
content: cleanedContent,
summary,
tableOfContents,
codeExamples,
imageReferences,
relatedSections,
keywords,
quality
};
}
/**
* Clean and normalize raw content
*/
cleanRawContent(content) {
if (!content)
return '';
return content
// Remove excessive whitespace
.replace(/\n\s*\n\s*\n/g, '\n\n')
.replace(/\t/g, ' ')
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
// Clean up common artifacts
.replace(/\u00A0/g, ' ') // Non-breaking spaces
.replace(/[\u2018\u2019]/g, "'") // Smart quotes
.replace(/[\u201C\u201D]/g, '"') // Smart double quotes
.replace(/\u2013/g, '-') // En dash
.replace(/\u2014/g, '--') // Em dash
// Remove navigation artifacts
.replace(/^\s*Skip to content.*$/gm, '')
.replace(/^\s*Developer.*Apple.*$/gm, '')
.replace(/^\s*Human Interface Guidelines.*$/gm, '')
// Trim and limit length
.trim()
.substring(0, 150000); // 150KB limit
}
/**
* Extract code examples from content
*/
extractCodeExamples(content) {
const codeExamples = [];
// Match code blocks
const codeBlockRegex = /```[\s\S]*?```/g;
const codeBlocks = content.match(codeBlockRegex) || [];
// Match inline code
const inlineCodeRegex = /`[^`\n]+`/g;
const inlineCodes = content.match(inlineCodeRegex) || [];
// Match SwiftUI/UIKit patterns
const swiftPatterns = [
/\b(?:struct|class|func|var|let)\s+\w+[\s\S]*?(?=\n\n|\n$)/g,
/\b(?:Button|Text|VStack|HStack|NavigationView)[\s\S]*?(?=\n\n|\n$)/g,
/\b(?:UIButton|UILabel|UIViewController|UIView)[\s\S]*?(?=\n\n|\n$)/g
];
swiftPatterns.forEach(pattern => {
const matches = content.match(pattern) || [];
codeExamples.push(...matches);
});
codeExamples.push(...codeBlocks, ...inlineCodes);
return [...new Set(codeExamples)].filter(code => code.length > 10);
}
/**
* Extract image references from content
*/
extractImageReferences(content) {
const imageRefs = [];
// Markdown images
const markdownImages = content.match(/!\[.*?\]\(.*?\)/g) || [];
imageRefs.push(...markdownImages);
// HTML images
const htmlImages = content.match(/<img[^>]*>/g) || [];
imageRefs.push(...htmlImages);
// Image URLs
const imageUrls = content.match(/https?:\/\/[^\s]*\.(jpg|jpeg|png|gif|svg|webp)/gi) || [];
imageRefs.push(...imageUrls);
return [...new Set(imageRefs)];
}
/**
* Extract headings from content
*/
extractHeadings(content) {
const headings = [];
// Match markdown headings
const headingRegex = /^(#{1,6})\s+(.+)$/gm;
let match;
while ((match = headingRegex.exec(content)) !== null) {
const level = match[1].length;
const text = match[2].trim();
const anchor = text.toLowerCase()
.replace(/[^a-z0-9\s]/g, '')
.replace(/\s+/g, '-');
headings.push({ level, text, anchor });
}
return headings;
}
/**
* Extract related sections from content
*/
extractRelatedSections(content) {
const relatedSections = [];
// Look for "See also", "Related", "Learn more" sections
const relatedPatterns = [
/(?:see also|related|learn more|additional resources):\s*(.+?)(?:\n\n|\n$)/gi,
/\[([^\]]+)\]\([^)]*human-interface-guidelines[^)]*\)/gi
];
relatedPatterns.forEach(pattern => {
let match;
while ((match = pattern.exec(content)) !== null) {
relatedSections.push(match[1].trim());
}
});
return [...new Set(relatedSections)].filter(section => section.length > 0);
}
/**
* Extract keywords from content
*/
extractKeywords(content, section) {
const keywords = new Set();
// Add section metadata
keywords.add(section.platform.toLowerCase());
keywords.add(section.category);
keywords.add(section.title.toLowerCase());
// Extract from content
const contentLower = content.toLowerCase();
// Add Apple design terms found in content
this.appleDesignTerms.forEach(term => {
if (contentLower.includes(term)) {
keywords.add(term);
}
});
// Add Apple platform terms found in content
this.applePlatformTerms.forEach(term => {
if (contentLower.includes(term)) {
keywords.add(term);
}
});
// Extract repeated words (potential keywords)
const words = contentLower.match(/\b[a-z]{3,}\b/g) || [];
const wordCount = new Map();
words.forEach(word => {
if (!this.isStopWord(word)) {
wordCount.set(word, (wordCount.get(word) || 0) + 1);
}
});
// Add frequently mentioned words
Array.from(wordCount.entries())
.filter(([, count]) => count >= 3)
.sort((a, b) => b[1] - a[1])
.slice(0, 20)
.forEach(([word]) => keywords.add(word));
return Array.from(keywords);
}
/**
* Generate table of contents from headings
*/
generateTableOfContents(headings) {
if (headings.length < 3)
return '';
const toc = ['## Table of Contents', ''];
headings.forEach(heading => {
if (heading.level <= 4) { // Only include up to h4
const indent = ' '.repeat(Math.max(0, heading.level - 2));
toc.push(`${indent}- [${heading.text}](#${heading.anchor})`);
}
});
toc.push('', '');
return toc.join('\n');
}
/**
* Generate content summary
*/
generateSummary(content, section) {
// Find first paragraph or substantial text block
const paragraphs = content.split('\n\n').filter(p => p.trim().length > 50);
if (paragraphs.length > 0) {
const firstParagraph = paragraphs[0]
.replace(/^#+\s+/, '') // Remove heading markers
.replace(/\*\*/g, '') // Remove bold markers
.trim();
if (firstParagraph.length > 100) {
return firstParagraph.substring(0, 300) + (firstParagraph.length > 300 ? '...' : '');
}
}
// Fallback summary
return `Guidelines for ${section.title} in ${section.platform} design. Part of Apple's Human Interface Guidelines covering ${section.category}.`;
}
/**
* Calculate comprehensive quality metrics
*/
calculateQualityMetrics(content, counts) {
let score = 0;
const contentLower = content.toLowerCase();
// Length score (0-0.25)
const lengthScore = Math.min(content.length / 5000, 1) * 0.25;
score += lengthScore;
// Structure score (0-0.25)
const structureScore = Math.min(counts.headingCount / 8, 1) * 0.25;
score += structureScore;
// Apple terms score (0-0.3)
const appleTermsFound = this.appleDesignTerms.filter(term => contentLower.includes(term)).length;
const appleTermsScore = Math.min(appleTermsFound / 10, 1) * 0.3;
score += appleTermsScore;
// Code examples boost (0-0.1)
if (counts.codeExamplesCount > 0) {
score += Math.min(counts.codeExamplesCount / 5, 1) * 0.1;
}
// Images boost (0-0.05)
if (counts.imageReferencesCount > 0) {
score += Math.min(counts.imageReferencesCount / 3, 1) * 0.05;
}
// Apple platform terms boost (0-0.05)
const platformTermsFound = this.applePlatformTerms.filter(term => contentLower.includes(term)).length;
if (platformTermsFound > 0) {
score += Math.min(platformTermsFound / 3, 1) * 0.05;
}
// Fallback content detection (major penalty)
const isFallbackContent = this.fallbackIndicators.some(indicator => contentLower.includes(indicator));
if (isFallbackContent) {
score = Math.min(score, 0.2); // Cap at 0.2 for fallback content
}
// Calculate confidence based on multiple factors
const confidence = this.calculateConfidence(content, score, isFallbackContent);
return {
score: Math.min(score, 1.0),
length: content.length,
structureScore,
appleTermsScore,
codeExamplesCount: counts.codeExamplesCount,
imageReferencesCount: counts.imageReferencesCount,
headingCount: counts.headingCount,
isFallbackContent,
extractionMethod: isFallbackContent ? 'fallback' : 'crawlee',
confidence
};
}
/**
* Calculate confidence in the extracted content
*/
calculateConfidence(content, score, isFallbackContent) {
if (isFallbackContent)
return 0.1;
let confidence = score;
// Boost confidence for specific quality indicators
const contentLower = content.toLowerCase();
// Has proper Apple attribution
if (contentLower.includes('apple inc') || contentLower.includes('human interface guidelines')) {
confidence += 0.1;
}
// Has technical content
if (contentLower.includes('swiftui') || contentLower.includes('uikit') || contentLower.includes('appkit')) {
confidence += 0.1;
}
// Has design guidance language
const designLanguage = ['should', 'avoid', 'consider', 'ensure', 'design', 'implement'];
const designTermsFound = designLanguage.filter(term => contentLower.includes(term)).length;
confidence += Math.min(designTermsFound / 10, 0.1);
return Math.min(confidence, 1.0);
}
/**
* Check if a word is a stop word
*/
isStopWord(word) {
const stopWords = new Set([
'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with',
'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'must',
'can', 'this', 'that', 'these', 'those', 'you', 'your', 'they', 'their', 'them',
'we', 'our', 'us', 'it', 'its', 'he', 'him', 'his', 'she', 'her', 'hers'
]);
return stopWords.has(word);
}
/**
* Validate content meets minimum quality standards
*/
isHighQualityContent(metrics) {
return (metrics.score >= 0.5 && // Minimum quality score
metrics.length >= 200 && // Minimum content length
!metrics.isFallbackContent && // Not fallback content
metrics.confidence >= 0.4 // Minimum confidence
);
}
/**
* Generate quality report for debugging
*/
generateQualityReport(metrics, section) {
return `
Quality Report for: ${section.title}
===========================================
Overall Score: ${metrics.score.toFixed(3)}
Confidence: ${metrics.confidence.toFixed(3)}
Length: ${metrics.length} characters
Structure Score: ${metrics.structureScore.toFixed(3)}
Apple Terms Score: ${metrics.appleTermsScore.toFixed(3)}
Headings: ${metrics.headingCount}
Code Examples: ${metrics.codeExamplesCount}
Image References: ${metrics.imageReferencesCount}
Extraction Method: ${metrics.extractionMethod}
Is Fallback: ${metrics.isFallbackContent}
High Quality: ${this.isHighQualityContent(metrics)}
`;
}
}
//# sourceMappingURL=hig-content-extractor.service.js.map