Spaces:
Sleeping
Sleeping
| import { JSDOM } from 'jsdom'; | |
| /** | |
| * HTML内の画像要素情報 | |
| */ | |
| export interface ImageElement { | |
| /** 要素のユニークID */ | |
| id: string; | |
| /** 元のsrc属性値 */ | |
| originalSrc: string; | |
| /** alt属性値 */ | |
| altText: string; | |
| /** 画像の幅 */ | |
| width: number; | |
| /** 画像の高さ */ | |
| height: number; | |
| /** 周辺のコンテキスト情報 */ | |
| context: ImageContext; | |
| /** HTMLでの位置(DOM内のセレクタ) */ | |
| selector: string; | |
| } | |
| /** | |
| * 画像のコンテキスト情報 | |
| */ | |
| export interface ImageContext { | |
| /** セクションのタイトル */ | |
| sectionTitle: string; | |
| /** 前後のテキスト */ | |
| surroundingText: string; | |
| /** セクションのタイプ(problem, solution, features等) */ | |
| sectionType: string; | |
| /** 親要素のクラス名 */ | |
| parentClasses: string[]; | |
| /** 画像の役割(メイン画像、アイコン、装飾等) */ | |
| imageRole: 'main' | 'icon' | 'decoration' | 'illustration'; | |
| } | |
| /** | |
| * HTMLから画像要素を抽出・解析するサービス | |
| */ | |
| export class HtmlImageAnalyzer { | |
| /** | |
| * HTMLから画像要素を抽出し、各要素の詳細情報を取得 | |
| */ | |
| static extractImageElements(html: string): ImageElement[] { | |
| const dom = new JSDOM(html); | |
| const document = dom.window.document; | |
| // プレースホルダー画像のパターン | |
| const placeholderPatterns = [/\/placeholder\.svg/, /https:\/\/images\.pexels\.com/, /https:\/\/via\.placeholder\.com/, /placeholder/i]; | |
| const imageElements: ImageElement[] = []; | |
| const images = document.querySelectorAll('img'); | |
| images.forEach((img, index) => { | |
| const src = img.getAttribute('src') || ''; | |
| const id = img.getAttribute('id') || ''; | |
| const alt = img.getAttribute('alt') || ''; | |
| // プレースホルダー画像かチェック | |
| const isPlaceholder = placeholderPatterns.some((pattern) => pattern.test(src)); | |
| if (isPlaceholder) { | |
| // 元のHTML内のIDがあれば必ずそれを使用 | |
| const existingId = img.getAttribute('id'); | |
| const imageId = existingId || `img-${index}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; | |
| const imageElement: ImageElement = { | |
| id: imageId, // 必ず既存IDまたは新規IDを使用 | |
| originalSrc: src, | |
| altText: alt, | |
| width: this.extractDimension(src, 'width') || 800, | |
| height: this.extractDimension(src, 'height') || 400, | |
| context: this.analyzeImageContext(img, document), | |
| selector: this.generateSelector(img), | |
| }; | |
| imageElements.push(imageElement); | |
| } | |
| }); | |
| return imageElements; | |
| } | |
| /** | |
| * 画像のコンテキスト情報を解析 | |
| */ | |
| private static analyzeImageContext(img: Element, document: Document): ImageContext { | |
| // 親セクションを特定 | |
| const section = img.closest('section'); | |
| const parentDiv = img.closest('div[class*="container"], div[class*="content"]'); | |
| // セクションタイトルを取得 | |
| const sectionTitle = this.findSectionTitle(section || parentDiv || img); | |
| // 周辺テキストを取得 | |
| const surroundingText = this.extractSurroundingText(img, 300); | |
| // セクションタイプを推定 | |
| const sectionType = this.detectSectionType(sectionTitle, surroundingText, img); | |
| // 親要素のクラス名を取得 | |
| const parentClasses = this.getParentClasses(img); | |
| // 画像の役割を推定 | |
| const imageRole = this.detectImageRole(img, parentClasses); | |
| return { | |
| sectionTitle, | |
| surroundingText, | |
| sectionType, | |
| parentClasses, | |
| imageRole, | |
| }; | |
| } | |
| /** | |
| * セクションのタイトルを取得 | |
| */ | |
| private static findSectionTitle(container: Element): string { | |
| if (!container) return ''; | |
| // 見出し要素を検索 | |
| const headings = container.querySelectorAll('h1, h2, h3, h4, h5, h6'); | |
| if (headings.length > 0) { | |
| return headings[0].textContent?.trim() || ''; | |
| } | |
| // タイトルクラスを持つ要素を検索 | |
| const titleElements = container.querySelectorAll('[class*="title"], [class*="heading"]'); | |
| if (titleElements.length > 0) { | |
| return titleElements[0].textContent?.trim() || ''; | |
| } | |
| return ''; | |
| } | |
| /** | |
| * 画像周辺のテキストを抽出 | |
| */ | |
| private static extractSurroundingText(img: Element, maxLength: number = 300): string { | |
| const container = img.closest('section, div[class*="container"], div[class*="content"]') || img.parentElement; | |
| if (!container) return ''; | |
| const text = container.textContent?.replace(/\s+/g, ' ').trim() || ''; | |
| return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; | |
| } | |
| /** | |
| * セクションタイプを推定 | |
| */ | |
| private static detectSectionType(title: string, text: string, img: Element): string { | |
| const combinedText = `${title} ${text}`.toLowerCase(); | |
| // キーワードベースの判定 | |
| if (combinedText.includes('問題') || combinedText.includes('課題') || combinedText.includes('悩み')) { | |
| return 'problem'; | |
| } | |
| if (combinedText.includes('解決') || combinedText.includes('ソリューション') || combinedText.includes('改善')) { | |
| return 'solution'; | |
| } | |
| if (combinedText.includes('特徴') || combinedText.includes('メリット') || combinedText.includes('強み')) { | |
| return 'features'; | |
| } | |
| if (combinedText.includes('サービス') || combinedText.includes('詳細') || combinedText.includes('機能')) { | |
| return 'service'; | |
| } | |
| if (combinedText.includes('流れ') || combinedText.includes('ステップ') || combinedText.includes('手順')) { | |
| return 'flow'; | |
| } | |
| if (combinedText.includes('実績') || combinedText.includes('事例') || combinedText.includes('成果')) { | |
| return 'achievements'; | |
| } | |
| if (combinedText.includes('お客様') || combinedText.includes('声') || combinedText.includes('評価')) { | |
| return 'testimonials'; | |
| } | |
| if (combinedText.includes('料金') || combinedText.includes('価格') || combinedText.includes('プラン')) { | |
| return 'pricing'; | |
| } | |
| // クラス名からの推定 | |
| const parentClasses = this.getParentClasses(img).join(' ').toLowerCase(); | |
| if (parentClasses.includes('hero') || parentClasses.includes('main')) { | |
| return 'hero'; | |
| } | |
| if (parentClasses.includes('feature') || parentClasses.includes('benefit')) { | |
| return 'features'; | |
| } | |
| if (parentClasses.includes('service') || parentClasses.includes('product')) { | |
| return 'service'; | |
| } | |
| return 'generic'; | |
| } | |
| /** | |
| * 親要素のクラス名を取得 | |
| */ | |
| private static getParentClasses(img: Element): string[] { | |
| const classes: string[] = []; | |
| let current = img.parentElement; | |
| let depth = 0; | |
| while (current && depth < 5) { | |
| if (current.className && typeof current.className === 'string') { | |
| classes.push(...current.className.split(' ').filter(Boolean)); | |
| } | |
| current = current.parentElement; | |
| depth++; | |
| } | |
| return classes; | |
| } | |
| /** | |
| * 画像の役割を推定 | |
| */ | |
| private static detectImageRole(img: Element, parentClasses: string[]): ImageContext['imageRole'] { | |
| const imgClasses = (img.className || '').toLowerCase(); | |
| const parentClassStr = parentClasses.join(' ').toLowerCase(); | |
| // アイコン系 | |
| if (imgClasses.includes('icon') || parentClassStr.includes('icon') || imgClasses.includes('avatar') || parentClassStr.includes('avatar')) { | |
| return 'icon'; | |
| } | |
| // メイン画像系 | |
| if (imgClasses.includes('hero') || imgClasses.includes('main') || parentClassStr.includes('hero') || parentClassStr.includes('main')) { | |
| return 'main'; | |
| } | |
| // 装飾系 | |
| if ( | |
| imgClasses.includes('decoration') || | |
| imgClasses.includes('bg') || | |
| parentClassStr.includes('decoration') || | |
| parentClassStr.includes('background') | |
| ) { | |
| return 'decoration'; | |
| } | |
| // デフォルトはイラスト | |
| return 'illustration'; | |
| } | |
| /** | |
| * URLパラメータから寸法を抽出 | |
| */ | |
| private static extractDimension(src: string, dimension: 'width' | 'height'): number | null { | |
| const match = src.match(new RegExp(`${dimension}=(\\d+)`)); | |
| return match ? parseInt(match[1], 10) : null; | |
| } | |
| /** | |
| * 要素のセレクタを生成 | |
| */ | |
| private static generateSelector(img: Element): string { | |
| const tagName = img.tagName.toLowerCase(); | |
| const id = img.id ? `#${img.id}` : ''; | |
| const classes = img.className ? `.${img.className.split(' ').join('.')}` : ''; | |
| if (id) return `${tagName}${id}`; | |
| if (classes) return `${tagName}${classes}`; | |
| // インデックスベースのセレクタ | |
| const parent = img.parentElement; | |
| if (parent) { | |
| const siblings = Array.from(parent.children).filter((child) => child.tagName === img.tagName); | |
| const index = siblings.indexOf(img); | |
| return `${parent.tagName.toLowerCase()} > ${tagName}:nth-of-type(${index + 1})`; | |
| } | |
| return tagName; | |
| } | |
| } | |