← Files DefuddleARCHIVED FILE

skills/defuddle-web-content/references/source/src/extractor-registry.ts

6.63 KB · Oct 2, 2026 · 00:35 UTC

↓ Download file

import { BaseExtractor, ExtractorOptions } from './extractors/_base';

// Extractors
import { RedditExtractor } from './extractors/reddit';
import { TwitterExtractor } from './extractors/twitter';
import { XArticleExtractor } from './extractors/x-article';
import { YoutubeExtractor } from './extractors/youtube';
import { BilibiliExtractor } from './extractors/bilibili';
import { HackerNewsExtractor } from './extractors/hackernews';
import { ChatGPTExtractor } from './extractors/chatgpt';
import { ClaudeExtractor } from './extractors/claude';
import { GrokExtractor } from './extractors/grok';
import { GeminiExtractor } from './extractors/gemini';
import { GitHubExtractor } from './extractors/github';
import { XOembedExtractor } from './extractors/x-oembed';
import { BbcodeDataExtractor } from './extractors/bbcode-data';
import { C2WikiExtractor } from './extractors/c2-wiki';
import { SubstackExtractor } from './extractors/substack';
import { NytimesExtractor } from './extractors/nytimes';
import { WikipediaExtractor } from './extractors/wikipedia';
import { LinkedInExtractor } from './extractors/linkedin';
import { ThreadsExtractor } from './extractors/threads';
import { BlueskyExtractor } from './extractors/bluesky';
import { DiscourseExtractor } from './extractors/discourse';
import { MediumExtractor } from './extractors/medium';
import { LeetCodeExtractor } from './extractors/leetcode';
import { LwnExtractor } from './extractors/lwn';
import { MastodonExtractor } from './extractors/mastodon';
import { GmailExtractor } from './extractors/gmail';

type ExtractorConstructor = new (document: Document, url: string, schemaOrgData?: any, options?: ExtractorOptions) => BaseExtractor;

interface ExtractorMapping {
	patterns: (string | RegExp)[];
	extractor: ExtractorConstructor;
}

export class ExtractorRegistry {
	private static mappings: ExtractorMapping[] = [];

	static initialize() {
		// Register all extractors with their URL patterns
		// X Article extractor must be registered BEFORE Twitter to take priority
		// DOM-based canExtract() determines if page has article content
		this.register({
			patterns: [
				'x.com',
				'twitter.com',
			],
			extractor: XArticleExtractor
		});

		this.register({
			patterns: [
				'twitter.com',
				/\/x\.com\/.*/,
			],
			extractor: TwitterExtractor
		});

		this.register({
			patterns: [
				'x.com',
				'twitter.com',
			],
			extractor: XOembedExtractor
		});

		this.register({
			patterns: [
				'mail.google.com',
			],
			extractor: GmailExtractor
		});

		this.register({
			patterns: [
				'reddit.com',
				'old.reddit.com',
				'new.reddit.com',
				/^https:\/\/[^\/]+\.reddit\.com/
			],
			extractor: RedditExtractor
		});

		this.register({
			patterns: [
				'youtube.com',
				'youtu.be',
				/youtube\.com\/watch\?v=.*/,
				/youtu\.be\/.*/
			],
			extractor: YoutubeExtractor
		});

		this.register({
			patterns: [
				'bilibili.com',
				/www\.bilibili\.com\/video\/BV[0-9A-Za-z]+/,
			],
			extractor: BilibiliExtractor
		});

		this.register({
			patterns: [
				'news.ycombinator.com',
			],
			extractor: HackerNewsExtractor
		});

		this.register({
			patterns: [
				/^https?:\/\/chatgpt\.com\/(c|share)\/.*/
			],
			extractor: ChatGPTExtractor
		});

		this.register({
			patterns: [
				'claude.ai',
				/^https?:\/\/claude\.ai\/(chat|share)\/.*/
			],
			extractor: ClaudeExtractor
		});

		this.register({
			patterns: [
				/^https?:\/\/grok\.com\/(chat|share)(\/.*)?$/
			],
			extractor: GrokExtractor,
		});

		this.register({
			patterns: [
				/^https?:\/\/gemini\.google\.com\/app\/.*/
			],
			extractor: GeminiExtractor
		});

		this.register({
			patterns: [
				'github.com',
				/^https?:\/\/github\.com\/.*/
			],
			extractor: GitHubExtractor
		});

		this.register({
			patterns: [
				'linkedin.com',
			],
			extractor: LinkedInExtractor
		});

		this.register({
			patterns: [
				'threads.net',
				'www.threads.com',
				'threads.com',
			],
			extractor: ThreadsExtractor
		});

		this.register({
			patterns: [
				'bsky.app',
			],
			extractor: BlueskyExtractor
		});

		this.register({
			patterns: [
				'medium.com',
				/\.medium\.com/,
			],
			extractor: MediumExtractor
		});

		this.register({
			patterns: [
				'wiki.c2.com',
			],
			extractor: C2WikiExtractor
		});

		this.register({
			patterns: [
				/^https?:\/\/substack\.com\/@[^/]+\/note\/.+/,
				/^https?:\/\/substack\.com\/home\/post\/p-\d+/,
				'substack.com',
			],
			extractor: SubstackExtractor
		});

		this.register({
			patterns: [
				'nytimes.com',
			],
			extractor: NytimesExtractor
		});

		this.register({
			patterns: [
				'wikipedia.org',
			],
			extractor: WikipediaExtractor
		});

		this.register({
			patterns: [/\/@[^/]+\/\d+/],
			extractor: MastodonExtractor
		});

		this.register({
			patterns: [/\/t\/[^/]+\/\d+/],
			extractor: DiscourseExtractor
		});

		this.register({
			patterns: [
				'leetcode.com',
			],
			extractor: LeetCodeExtractor
		});

		this.register({
			patterns: [
				'lwn.net',
			],
			extractor: LwnExtractor
		});

		this.register({
			patterns: [/.*/],
			extractor: BbcodeDataExtractor
		});
	}

	static register(mapping: ExtractorMapping) {
		this.mappings.push(mapping);
	}

	static findExtractor(document: Document, url: string, schemaOrgData?: any, options?: ExtractorOptions): BaseExtractor | null {
		return this.findByPredicate(document, url, schemaOrgData, e => e.canExtract(), options);
	}

	static findAsyncExtractor(document: Document, url: string, schemaOrgData?: any, options?: ExtractorOptions): BaseExtractor | null {
		return this.findByPredicate(document, url, schemaOrgData, e => e.canExtractAsync(), options);
	}

	static findPreferredAsyncExtractor(document: Document, url: string, schemaOrgData?: any, options?: ExtractorOptions): BaseExtractor | null {
		return this.findByPredicate(document, url, schemaOrgData, e => e.canExtractAsync() && e.prefersAsync(), options);
	}

	private static findByPredicate(
		document: Document, url: string, schemaOrgData: any | undefined,
		predicate: (instance: BaseExtractor) => boolean,
		options?: ExtractorOptions
	): BaseExtractor | null {
		try {
			const domain = new URL(url).hostname;

			for (const { patterns, extractor } of this.mappings) {
				const matches = patterns.some(pattern => {
					if (pattern instanceof RegExp) {
						return pattern.test(url);
					}
					return domain.includes(pattern);
				});

				if (matches) {
					const instance = new extractor(document, url, schemaOrgData, options);
					if (predicate(instance)) {
						return instance;
					}
				}
			}

			return null;

		} catch (error) {
			console.error('Error finding extractor:', error);
			return null;
		}
	}
}

// Initialize extractors
ExtractorRegistry.initialize();

SHA-256: cd152281bfc0f44ddfa984a7c566a9fa9d198102d370b2c1157aa91fdd01ae27