ai-seo
Version:
AI-native JSON-LD schema utility with automated URL-to-Schema generation, intelligent content analysis, caching, rate limiting, performance monitoring, and AI optimization (ChatGPT, Voice). Complete automation & scale features. Zero runtime dependencies.
300 lines (252 loc) • 7.67 kB
JavaScript
// HTMLParser - v1.12.0
// Parse HTML and extract metadata for schema generation
/**
* HTMLParser
*
* Extracts structured content and metadata from HTML documents.
* Supports meta tags, OpenGraph, Twitter Cards, and existing schemas.
*
* @example
* const parsed = HTMLParser.parse(html, url);
* console.log(parsed.title); // Page title
*/
export class HTMLParser {
/**
* Parse HTML and extract all metadata
*
* @param {string} html - Raw HTML content
* @param {string} url - Source URL
* @returns {Object} Parsed content and metadata
*/
static parse(html, url) {
if (!html || typeof html !== 'string') {
throw new Error('Invalid HTML content');
}
try {
return {
url,
title: this._extractTitle(html),
description: this._extractDescription(html),
content: this._extractMainContent(html),
images: this._extractImages(html, url),
meta: this._extractMetaTags(html),
openGraph: this._extractOpenGraph(html),
twitterCard: this._extractTwitterCard(html),
existing: this._extractExistingSchemas(html),
structured: this._extractStructuredData(html)
};
} catch (error) {
throw new Error(`HTML parsing failed: ${error.message}`);
}
}
/**
* Extract page title
* @private
*/
static _extractTitle(html) {
// Try <title> tag first
const titleMatch = html.match(/<title[^>]*>(.*?)<\/title>/is);
if (titleMatch && titleMatch[1]) {
return this._cleanText(titleMatch[1]);
}
// Try og:title
const ogTitle = html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']*)["']/i);
if (ogTitle && ogTitle[1]) {
return this._cleanText(ogTitle[1]);
}
// Try h1
const h1Match = html.match(/<h1[^>]*>(.*?)<\/h1>/is);
if (h1Match && h1Match[1]) {
return this._cleanText(h1Match[1]);
}
return '';
}
/**
* Extract meta description
* @private
*/
static _extractDescription(html) {
// Try meta description
const metaDesc = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["']/i);
if (metaDesc && metaDesc[1]) {
return this._cleanText(metaDesc[1]);
}
// Try og:description
const ogDesc = html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']*)["']/i);
if (ogDesc && ogDesc[1]) {
return this._cleanText(ogDesc[1]);
}
return '';
}
/**
* Extract main content text
* @private
*/
static _extractMainContent(html) {
// Remove script and style tags
let content = html.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '');
content = content.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '');
// Try to find main content area
const mainMatch = content.match(/<main[^>]*>([\s\S]*?)<\/main>/i) ||
content.match(/<article[^>]*>([\s\S]*?)<\/article>/i) ||
content.match(/<div[^>]*class=["'][^"']*content[^"']*["'][^>]*>([\s\S]*?)<\/div>/i);
if (mainMatch && mainMatch[1]) {
content = mainMatch[1];
}
// Remove all HTML tags
content = content.replace(/<[^>]+>/g, ' ');
// Clean up whitespace
content = content.replace(/\s+/g, ' ').trim();
// Limit length
return content.substring(0, 5000);
}
/**
* Extract images from HTML
* @private
*/
static _extractImages(html, baseUrl) {
const images = [];
const imageRegex = /<img[^>]*src=["']([^"']*)["']/gi;
let match;
while ((match = imageRegex.exec(html)) !== null) {
if (match[1]) {
images.push(this._resolveUrl(match[1], baseUrl));
}
}
// Also check og:image
const ogImage = html.match(/<meta[^>]*property=["']og:image["'][^>]*content=["']([^"']*)["']/i);
if (ogImage && ogImage[1]) {
images.unshift(this._resolveUrl(ogImage[1], baseUrl));
}
// Remove duplicates
return [...new Set(images)];
}
/**
* Extract all meta tags
* @private
*/
static _extractMetaTags(html) {
const meta = {};
const metaRegex = /<meta[^>]*>/gi;
let match;
while ((match = metaRegex.exec(html)) !== null) {
const tag = match[0];
// Extract name/property and content
const nameMatch = tag.match(/(?:name|property)=["']([^"']*)["']/i);
const contentMatch = tag.match(/content=["']([^"']*)["']/i);
if (nameMatch && contentMatch) {
const key = nameMatch[1].toLowerCase();
meta[key] = contentMatch[1];
}
}
return meta;
}
/**
* Extract OpenGraph tags
* @private
*/
static _extractOpenGraph(html) {
const og = {};
const ogRegex = /<meta[^>]*property=["']og:([^"']*)["'][^>]*content=["']([^"']*)["']/gi;
let match;
while ((match = ogRegex.exec(html)) !== null) {
og[match[1]] = match[2];
}
return og;
}
/**
* Extract Twitter Card tags
* @private
*/
static _extractTwitterCard(html) {
const twitter = {};
const twitterRegex = /<meta[^>]*name=["']twitter:([^"']*)["'][^>]*content=["']([^"']*)["']/gi;
let match;
while ((match = twitterRegex.exec(html)) !== null) {
twitter[match[1]] = match[2];
}
return twitter;
}
/**
* Extract existing JSON-LD schemas
* @private
*/
static _extractExistingSchemas(html) {
const schemas = [];
const scriptRegex = /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi;
let match;
while ((match = scriptRegex.exec(html)) !== null) {
try {
const schema = JSON.parse(match[1]);
schemas.push(schema);
} catch (error) {
// Invalid JSON, skip
}
}
return schemas;
}
/**
* Extract microdata and RDFa
* @private
*/
static _extractStructuredData(html) {
const structured = [];
// Look for itemscope (microdata)
const itemscopeRegex = /<[^>]*itemscope[^>]*itemtype=["']([^"']*)["'][^>]*>([\s\S]*?)<\/[^>]*>/gi;
let match;
while ((match = itemscopeRegex.exec(html)) !== null) {
structured.push({
type: 'microdata',
itemType: match[1]
});
}
return structured;
}
/**
* Resolve relative URLs to absolute
* @private
*/
static _resolveUrl(url, baseUrl) {
if (!url) return '';
// Already absolute
if (url.startsWith('http://') || url.startsWith('https://')) {
return url;
}
// Protocol-relative
if (url.startsWith('//')) {
return 'https:' + url;
}
// Absolute path
if (url.startsWith('/')) {
try {
const base = new URL(baseUrl);
return `${base.protocol}//${base.host}${url}`;
} catch {
return url;
}
}
// Relative path
try {
return new URL(url, baseUrl).href;
} catch {
return url;
}
}
/**
* Clean text content
* @private
*/
static _cleanText(text) {
if (!text) return '';
// Remove HTML entities
text = text.replace(/ /g, ' ');
text = text.replace(/&/g, '&');
text = text.replace(/</g, '<');
text = text.replace(/>/g, '>');
text = text.replace(/"/g, '"');
text = text.replace(/'/g, "'");
// Clean whitespace
text = text.replace(/\s+/g, ' ').trim();
return text;
}
}