{"id":29673718,"url":"https://github.com/hanivan/nestjs-html-parser","last_synced_at":"2026-03-09T13:05:47.559Z","repository":{"id":297018300,"uuid":"995391797","full_name":"Hanivan/nestjs-html-parser","owner":"Hanivan","description":null,"archived":false,"fork":false,"pushed_at":"2026-01-16T09:19:53.000Z","size":1185,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-01-16T23:44:58.068Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Hanivan.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-06-03T12:12:29.000Z","updated_at":"2026-01-16T09:19:59.000Z","dependencies_parsed_at":"2025-06-03T22:35:09.515Z","dependency_job_id":"422eecd3-dcc6-480f-8d20-6d34d3f53d6f","html_url":"https://github.com/Hanivan/nestjs-html-parser","commit_stats":null,"previous_names":["hanivan/nestjs-html-parser"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/Hanivan/nestjs-html-parser","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hanivan%2Fnestjs-html-parser","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hanivan%2Fnestjs-html-parser/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hanivan%2Fnestjs-html-parser/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hanivan%2Fnestjs-html-parser/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Hanivan","download_url":"https://codeload.github.com/Hanivan/nestjs-html-parser/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Hanivan%2Fnestjs-html-parser/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30297111,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-09T11:12:22.024Z","status":"ssl_error","status_checked_at":"2026-03-09T11:10:54.577Z","response_time":61,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-07-22T22:07:46.389Z","updated_at":"2026-03-09T13:05:47.546Z","avatar_url":"https://github.com/Hanivan.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# ⚠️ **IMPORTANT: This package is archived and no longer maintained**\n\n\u003e **This package has been renamed and migrated.** Please use the new package instead:\n\u003e **[@hanivanrizky/nestjs-xpath-parser](https://github.com/Hanivan/nestjs-xpath-parser)**\n\u003e\n\u003e This repository is kept for historical reference only. No updates, bug fixes, or support will be provided.\n\n## Migration Guide\n\n### Old Package (Archived)\n```bash\nnpm uninstall @hanivanrizky/nestjs-html-parser\n```\n\n### New Package (Active)\n```bash\nnpm install @hanivanrizky/nestjs-xpath-parser\n```\n\nThe new package has improved features, better documentation, and active maintenance.\n\n---\n\n# @hanivanrizky/nestjs-html-parser\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"http://nestjs.com/\" target=\"blank\"\u003e\u003cimg src=\"https://nestjs.com/img/logo-small.svg\" width=\"120\" alt=\"Nest Logo\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003eA powerful NestJS package for parsing HTML content using XPath (primary) and CSS selectors (secondary) with comprehensive extraction capabilities.\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@hanivanrizky/nestjs-html-parser\" target=\"_blank\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@hanivanrizky/nestjs-html-parser.svg\" alt=\"NPM Version\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@hanivanrizky/nestjs-html-parser\" target=\"_blank\"\u003e\u003cimg src=\"https://img.shields.io/npm/l/@hanivanrizky/nestjs-html-parser.svg\" alt=\"Package License\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.npmjs.com/package/@hanivanrizky/nestjs-html-parser\" target=\"_blank\"\u003e\u003cimg src=\"https://img.shields.io/npm/dm/@hanivanrizky/nestjs-html-parser.svg\" alt=\"NPM Downloads\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n  - [Import the Module](#import-the-module)\n  - [Async Configuration](#async-configuration)\n  - [Inject the Service](#inject-the-service)\n- [Core Features](#core-features)\n  - [HTML Fetching with Response Metadata](#html-fetching-with-response-metadata)\n  - [Data Extraction Methods](#data-extraction-methods)\n  - [Proxy Support](#proxy-support)\n  - [Error Handling](#error-handling)\n- [TypeScript Definitions \u0026 Types](#typescript-definitions--types)\n  - [Complete Interface Definitions](#complete-interface-definitions)\n  - [Implementation Guide](#implementation-guide)\n- [API Reference](#api-reference)\n  - [Core Methods](#core-methods)\n  - [Advanced Methods](#advanced-methods)\n- [Development](#development)\n- [Contributing](#contributing)\n- [License](#license)\n\n## Features\n\n- **🎯 XPath Support (Primary)**: Full XPath 1.0 support for precise element selection\n- **🎨 CSS Selectors (Secondary)**: jQuery-style CSS selectors for familiar syntax\n- **📋 Multiple Extraction Methods**: Single values, multiple values, attributes, and structured data\n- **🔍 Element Analysis**: Check existence and count elements\n- **📊 Structured Extraction**: Extract data using schema objects for complex data structures\n- **📚 List Extraction**: Extract arrays of structured data\n- **🌐 HTTP Fetching**: Built-in HTML fetching with customizable options\n- **🛡️ Error Handling**: Graceful error handling and fallbacks\n- **🔄 Random User Agents**: Built-in random user agent generation for stealth scraping\n- **🔗 Proxy Support**: HTTP, HTTPS, and SOCKS proxy support with authentication\n- **🔁 Retry Logic**: Configurable retry mechanism with exponential backoff\n- **🔇 Verbose \u0026 Logger Level Control**: Optional verbose mode for debugging and configurable logger level (debug, log, warn, error, verbose)\n- **🔒 SSL Error Handling**: Comprehensive SSL certificate error handling and bypass options\n- **💀 Dead Domain Support**: Advanced error categorization for dead/unreachable domains\n- **🔄 Smart Retry Logic**: Error-type-specific retry strategies for different network issues\n- **🎯 TypeScript Generics**: Full generic type support for compile-time type safety\n- **🧪 Fully Tested**: Comprehensive test suite with real-world examples\n\n## Installation\n\n```bash\nyarn add @hanivanrizky/nestjs-html-parser\n# or\nnpm install @hanivanrizky/nestjs-html-parser\n```\n\n## Quick Start\n\n### Import the Module\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { HtmlParserModule } from '@hanivanrizky/nestjs-html-parser';\n\n@Module({\n  imports: [\n    HtmlParserModule.forRoot(), // Default: loggerLevel: ['log', 'error'] (production ready)\n    // Or override for development:\n    HtmlParserModule.forRoot({ loggerLevel: 'debug' }),\n    // Or enable multiple levels:\n    HtmlParserModule.forRoot({ loggerLevel: ['debug', 'warn'] }),\n  ],\n})\nexport class AppModule {}\n```\n\n#### Async Configuration\n\n```typescript\nimport { Module } from '@nestjs/common';\nimport { ConfigModule, ConfigService } from '@nestjs/config';\nimport { HtmlParserModule } from '@hanivanrizky/nestjs-html-parser';\n\n@Module({\n  imports: [\n    ConfigModule.forRoot(),\n    HtmlParserModule.forRootAsync({\n      imports: [ConfigModule],\n      useFactory: (configService: ConfigService) =\u003e ({\n        loggerLevel: configService.get\u003c'debug'|'log'|'warn'|'error'|'verbose'|('debug'|'log'|'warn'|'error'|'verbose')[]\u003e('HTML_PARSER_LOGGER_LEVEL', 'warn'),\n      }),\n      inject: [ConfigService],\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### Inject the Service\n\n```typescript\nimport { Injectable } from '@nestjs/common';\nimport { HtmlParserService } from '@hanivanrizky/nestjs-html-parser';\n\n@Injectable()\nexport class YourService {\n  constructor(private readonly htmlParser: HtmlParserService) {}\n\n  async parseHackerNews() {\n    const response = await this.htmlParser.fetchHtml('https://news.ycombinator.com/');\n    \n    // Extract page title\n    const title = this.htmlParser.extractSingle(response.data, '//title/text()');\n    \n    // Extract all story titles\n    const storyTitles = this.htmlParser.extractMultiple(\n      response.data, \n      '//span[@class=\"titleline\"]/a/text()'\n    );\n    \n    return { title, storyTitles, status: response.status };\n  }\n}\n```\n\n## Core Features\n\n### HTML Fetching with Response Metadata\n\n```typescript\nconst response = await htmlParser.fetchHtml('https://example.com', {\n  timeout: 10000,\n  headers: { 'User-Agent': 'Custom Agent' },\n  retryOnErrors: {\n    ssl: true,\n    timeout: true,\n    dns: true,\n    connectionRefused: true\n  }\n});\n\n// Response includes:\n// - data: HTML content\n// - headers: Response headers\n// - status: HTTP status code\n// - statusText: HTTP status text\n```\n\n### Data Extraction Methods\n\n```typescript\n// Single value extraction\nconst title = htmlParser.extractSingle(html, '//title/text()');\n\n// Multiple values\nconst links = htmlParser.extractMultiple(html, '//a/text()');\n\n// Attribute extraction\nconst urls = htmlParser.extractAttributes(html, '//a', 'href');\n\n// Structured data extraction with advanced transform\nclass UppercasePipe {\n  transform(value: string) {\n    return value.toUpperCase();\n  }\n}\nclass SuffixPipe {\n  constructor(private suffix: string) {}\n  transform(value: string) {\n    return value + this.suffix;\n  }\n}\nconst schema = {\n  title: {\n    selector: '//h1/text()',\n    type: 'xpath',\n    transform: [\n      (title: string) =\u003e title.trim(),\n      UppercasePipe,\n      new SuffixPipe(' [ADVANCED]'),\n    ],\n  },\n  episode: {\n    selector: '//div[@class=\"epz\"]',\n    type: 'xpath',\n    transform: [\n      (text: any) =\u003e {\n        if (typeof text !== 'string') return 0;\n        let match = text.match(/Episode\\s+(\\d+)/i);\n        if (!match) match = text.match(/(\\d+)/);\n        return match ? parseInt(match[1]) : 0;\n      },\n      new SuffixPipe(' (ep)'),\n    ],\n  },\n};\nconst data = htmlParser.extractStructured(html, schema);\n```\n\n### Proxy Support\n\n```typescript\nconst proxyConfig = {\n  url: 'http://proxy.example.com:8080',\n  type: 'http',\n  username: 'user',\n  password: 'pass'\n};\n\nconst html = await htmlParser.fetchHtml('https://example.com', {\n  proxy: proxyConfig,\n  useRandomUserAgent: true\n});\n```\n\n### SSL/TLS Configuration \u0026 Security\n\nThe HTML Parser Service provides three levels of SSL configuration for handling different certificate scenarios:\n\n#### 🔒 SSL Configuration Options (Independent Controls)\n\nThe service provides **three independent SSL configuration options** that can be used alone or in combination:\n\n1. **`rejectUnauthorized: false`** - Accept invalid/self-signed certificates\n2. **`disableServerIdentityCheck: true`** - Skip hostname validation (independent option)\n3. **`ignoreSSLErrors: true`** - Disable all SSL validation (⚠️ Use with extreme caution)\n\n**🔑 Key Point:** `disableServerIdentityCheck` is **fully independent** of `ignoreSSLErrors` and only controls hostname validation.\n\n```typescript\n// Default: Full SSL validation (recommended for production)\nconst response = await htmlParser.fetchHtml('https://trusted-site.com');\n\n// Accept self-signed certificates only\nconst response = await htmlParser.fetchHtml('https://self-signed-site.com', {\n  rejectUnauthorized: false\n});\n\n// Skip only hostname validation (certificate still validated)\nconst response = await htmlParser.fetchHtml('https://hostname-mismatch-site.com', {\n  disableServerIdentityCheck: true  // Works independently\n});\n\n// Combine: Accept invalid certs + skip hostname validation\nconst response = await htmlParser.fetchHtml('https://problematic-site.com', {\n  rejectUnauthorized: false,\n  disableServerIdentityCheck: true  // Both options work together\n});\n\n// Disable all SSL validation (⚠️ DANGEROUS - development only)\nconst response = await htmlParser.fetchHtml('https://any-ssl-issue-site.com', {\n  ignoreSSLErrors: true  // Overrides all SSL checks\n});\n\n// Mixed configuration: Disable all SSL but explicitly control hostname check\nconst response = await htmlParser.fetchHtml('https://mixed-config-site.com', {\n  ignoreSSLErrors: true,\n  disableServerIdentityCheck: false  // Independent: hostname check still works\n});\n```\n\n#### ⚠️ **CRITICAL SECURITY WARNING: `disableServerIdentityCheck`**\n\nThe `disableServerIdentityCheck` parameter bypasses server name indication (SNI) validation, which is a **critical security mechanism** that:\n\n- **Prevents man-in-the-middle attacks** by ensuring you're connecting to the intended server\n- **Validates hostname matches** between the certificate and the requested domain\n- **Protects against certificate spoofing** and domain impersonation\n\n**🚨 NEVER use `disableServerIdentityCheck: true` in production unless:**\n- You fully understand the security implications\n- You have other security measures in place (e.g., certificate pinning)\n- You are connecting to a known, trusted internal service with hostname mismatches\n- You are in a controlled testing environment\n\n**✅ Safe Use Cases:**\n- Development environments with self-hosted services\n- Testing against staging servers with certificate issues\n- Internal corporate networks with hostname mismatches\n- Temporary workarounds during certificate renewal periods\n\n**❌ NEVER Use In:**\n- Production applications handling sensitive data\n- Public-facing services\n- Financial or healthcare applications\n- Any scenario where security is paramount\n\n```typescript\n// ❌ DANGEROUS: Complete SSL bypass (never in production)\nconst response = await htmlParser.fetchHtml(url, {\n  ignoreSSLErrors: true  // Disables ALL SSL validation including hostname check\n});\n\n// ⚠️ SELECTIVE: Independent hostname validation control\nconst response = await htmlParser.fetchHtml(url, {\n  ignoreSSLErrors: true,\n  disableServerIdentityCheck: false  // Still enforces hostname validation despite ignoreSSLErrors\n});\n\n// ✅ BETTER: Minimal SSL relaxation\nconst response = await htmlParser.fetchHtml(url, {\n  rejectUnauthorized: false,  // Accept invalid certificates only\n  disableServerIdentityCheck: false  // Keep hostname validation (default)\n});\n\n// ✅ TARGETED: Skip only hostname validation\nconst response = await htmlParser.fetchHtml(url, {\n  disableServerIdentityCheck: true  // Only bypasses hostname check, certificate still validated\n});\n\n// ✅ PRODUCTION: Full SSL validation (default)\nconst response = await htmlParser.fetchHtml(url, {\n  // All SSL validations enabled by default\n});\n```\n\n### Error Handling\n\n```typescript\ntry {\n  const response = await htmlParser.fetchHtml('https://example.com', {\n    rejectUnauthorized: false,\n    retryOnErrors: {\n      ssl: true,\n      timeout: true,\n      dns: true\n    }\n  });\n} catch (error) {\n  // Error is categorized by type (ssl, dns, timeout, etc.)\n  console.error(`Failed: ${error.message}`);\n}\n```\n\n## TypeScript Definitions \u0026 Types\n\n### Complete Interface Definitions\n\n```typescript\n// ===== CORE SERVICE INTERFACE =====\ninterface HtmlParserService {\n  // Main HTML fetching method\n  fetchHtml(url: string, options?: HtmlParserOptions): Promise\u003cHtmlFetchResponse\u003e;\n  \n  // Single value extraction methods\n  extractSingle\u003cT = string\u003e(\n    html: string,\n    selector: string,\n    type?: 'xpath' | 'css',           // Default: 'xpath'\n    attribute?: string,\n    options?: ExtractionOptions\u003cT\u003e\n  ): T | null;\n  \n  extractText\u003cT = string\u003e(\n    html: string,\n    selector: string,\n    type?: 'xpath' | 'css',           // Default: 'xpath'\n    options?: ExtractionOptions\u003cT\u003e\n  ): T | null;\n  \n  // Multiple value extraction methods  \n  extractMultiple\u003cT = string\u003e(\n    html: string,\n    selector: string,\n    type?: 'xpath' | 'css',           // Default: 'xpath'\n    attribute?: string,\n    options?: ExtractionOptions\u003cT\u003e\n  ): T[];\n  \n  extractAttributes\u003cT = string\u003e(\n    html: string,\n    selector: string,\n    attribute: string,\n    type?: 'xpath' | 'css',           // Default: 'xpath'\n    options?: ExtractionOptions\u003cT\u003e\n  ): T[];\n  \n  // Structured extraction methods\n  extractStructured\u003cT = Record\u003cstring, any\u003e\u003e(\n    html: string,\n    schema: ExtractionSchema\u003cT\u003e,\n    options?: { verbose?: boolean }\n  ): T;\n  \n  extractStructuredList\u003cT = Record\u003cstring, any\u003e\u003e(\n    html: string,\n    containerSelector: string,\n    schema: ExtractionSchema\u003cT\u003e,\n    containerType?: 'xpath' | 'css',  // Default: 'xpath'\n    options?: { verbose?: boolean }\n  ): T[];\n  \n  // Utility methods\n  exists(html: string, selector: string, type?: 'xpath' | 'css', options?: { verbose?: boolean }): boolean;\n  count(html: string, selector: string, type?: 'xpath' | 'css', options?: { verbose?: boolean }): number;\n  \n  // Advanced utility methods\n  getRandomUserAgent(): Promise\u003cstring\u003e;\n  testProxy(proxy: ProxyConfig, testUrl?: string): Promise\u003cboolean\u003e;\n}\n\n// ===== CONFIGURATION TYPES =====\ninterface HtmlParserOptions {\n  timeout?: number;                    // Request timeout in milliseconds (default: 10000)\n  headers?: Record\u003cstring, string\u003e;    // Custom headers to send with request\n  userAgent?: string;                  // Custom user agent string (default: Mozilla/5.0...)\n  useRandomUserAgent?: boolean;        // Use random user agent (default: false)\n  proxy?: ProxyConfig;                 // Proxy configuration\n  retries?: number;                    // Number of retry attempts (default: 3)\n  retryDelay?: number;                 // Delay between retries in ms (default: 1000)\n  verbose?: boolean;                   // Enable verbose logging (default: false)\n  rejectUnauthorized?: boolean;        // Reject unauthorized SSL certificates (default: true)\n  ignoreSSLErrors?: boolean;           // Skip SSL certificate verification (default: false)\n  disableServerIdentityCheck?: boolean; // ⚠️ SECURITY WARNING: Disable server name indication (SNI) validation (default: false)\n  maxRedirects?: number;               // Maximum redirects to follow (default: 5)\n  retryOnErrors?: {                    // Configure retry behavior for specific error types\n    ssl?: boolean;                     // Retry on SSL/TLS errors (default: false)\n    timeout?: boolean;                 // Retry on connection timeout (default: true)\n    dns?: boolean;                     // Retry on DNS resolution errors (default: true)\n    connectionRefused?: boolean;       // Retry on connection refused errors (default: true)\n  };\n}\n\ninterface HtmlFetchResponse {\n  data: string;                        // HTML content of the fetched page\n  headers: Record\u003cstring, string\u003e;     // HTTP response headers as key-value pairs\n  status: number;                      // HTTP status code (e.g., 200, 404, 500)\n  statusText: string;                  // HTTP status text (e.g., 'OK', 'Not Found')\n}\n\ninterface ProxyConfig {\n  url: string;                         // Proxy server URL (e.g., 'http://proxy.example.com:8080')\n  type?: 'http' | 'https' | 'socks4' | 'socks5';  // Type of proxy server (auto-detected from URL)\n  username?: string;                   // Username for proxy authentication\n  password?: string;                   // Password for proxy authentication\n}\n\n// ===== EXTRACTION TYPES =====\ntype TransformFunction = (value: any) =\u003e any;\ntype TransformObject = { transform: (value: any) =\u003e any };\ntype TransformClass = new (...args: any[]) =\u003e TransformObject;\ntype TransformType =\n  | TransformFunction\n  | TransformObject\n  | TransformClass\n  | Array\u003cTransformFunction | TransformObject | TransformClass\u003e;\n\ninterface ExtractionOptions\u003cT = any\u003e {\n  verbose?: boolean;                   // Enable verbose logging for this extraction\n  transform?: TransformType;           // Transform to apply to extracted value\n}\n\ninterface ExtractionField\u003cT = any\u003e {\n  selector: string;                    // CSS selector or XPath expression\n  type: 'xpath' | 'css';              // Type of selector being used\n  attribute?: string;                  // HTML attribute to extract from selected element\n  transform?: TransformType;           // Transform to apply to extracted value\n  multiple?: boolean;                  // If true, extract array of values instead of single value\n  raw?: boolean;                       // If true, return raw HTML of matched element(s)\n}\n\ninterface ExtractionSchema\u003cT = Record\u003cstring, any\u003e\u003e {\n  [K in keyof T]: ExtractionField\u003cT[K]\u003e;\n}\n\n// ===== MODULE CONFIGURATION TYPES =====\ntype LogLevel = 'debug' | 'log' | 'warn' | 'error' | 'verbose'; // same LogLevel type from @nestjs/common\n\ninterface HtmlParserConfig {\n  loggerLevel?: LogLevel | Array\u003cLogLevel\u003e;  // Default: ['log', 'error']\n}\n\ninterface HtmlParserModuleAsyncOptions extends Pick\u003cModuleMetadata, 'imports'\u003e {\n  useExisting?: Type\u003cHtmlParserConfigFactory\u003e;\n  useClass?: Type\u003cHtmlParserConfigFactory\u003e;\n  useFactory?: (...args: any[]) =\u003e Promise\u003cHtmlParserConfig\u003e | HtmlParserConfig;\n  inject?: any[];\n}\n\ninterface HtmlParserConfigFactory {\n  createHtmlParserConfig(): Promise\u003cHtmlParserConfig\u003e | HtmlParserConfig;\n}\n```\n\n### Implementation Guide\n\n#### ✅ **Production-Ready Configuration Patterns**\n\n**For Health Checks / Monitoring:**\n```typescript\nconst healthCheckOptions: HtmlParserOptions = {\n  timeout: 15000,                      // Shorter timeout for health checks\n  useRandomUserAgent: true,            // Avoid being blocked\n  retries: 1,                          // Fast fail for health checks\n  retryDelay: 500,                     // Quick retry for transient issues\n  verbose: false,                      // Keep logging minimal in production\n  rejectUnauthorized: false,           // Accept self-signed certificates\n  ignoreSSLErrors: true,               // ⚠️ Ignore SSL errors for monitoring (development only)\n  disableServerIdentityCheck: false,   // ✅ Independent: Keep hostname validation even with ignoreSSLErrors\n  maxRedirects: 2,                     // Limit redirects for performance\n  retryOnErrors: {\n    ssl: false,                        // Don't retry SSL errors\n    timeout: false,                    // Don't retry timeouts in health checks\n    dns: true,                         // Retry DNS errors only\n    connectionRefused: false,          // Don't retry connection refused\n  },\n};\n\nconst response: HtmlFetchResponse = await htmlParser.fetchHtml(url, healthCheckOptions);\n```\n\n**For Web Scraping / Data Extraction:**\n```typescript\nconst scrapingOptions: HtmlParserOptions = {\n  timeout: 30000,                      // Longer timeout for content loading\n  useRandomUserAgent: true,            // Rotate user agents to avoid blocking\n  retries: 3,                          // More persistent for data extraction\n  retryDelay: 2000,                    // Respect rate limits\n  verbose: false,                      // Enable only for debugging\n  rejectUnauthorized: false,           // Handle various SSL configurations\n  disableServerIdentityCheck: true,    // ⚠️ Independent: Skip only hostname validation\n  ignoreSSLErrors: false,              // Prefer minimal SSL relaxation (keeps certificate validation)\n  maxRedirects: 5,                     // Follow redirects for content\n  retryOnErrors: {\n    ssl: false,                        // SSL errors usually permanent\n    timeout: true,                     // Retry timeouts for slow sites\n    dns: true,                         // Retry DNS resolution failures\n    connectionRefused: false,          // Usually indicates server issues\n  },\n};\n\nconst response: HtmlFetchResponse = await htmlParser.fetchHtml(url, scrapingOptions);\n```\n\n**For Development / Testing:**\n```typescript\nconst devOptions: HtmlParserOptions = {\n  timeout: 10000,\n  useRandomUserAgent: false,           // Consistent user agent for testing\n  retries: 1,                          // Fail fast during development\n  retryDelay: 1000,\n  verbose: true,                       // Enable detailed logging\n  rejectUnauthorized: false,           // Handle local/test SSL certificates\n  disableServerIdentityCheck: true,    // ✅ Independent: OK for development/testing only\n  ignoreSSLErrors: false,              // Prefer targeted SSL relaxation (keeps certificate validation)\n  maxRedirects: 3,\n  retryOnErrors: {\n    ssl: false,\n    timeout: false,                    // Don't retry to see issues quickly\n    dns: true,\n    connectionRefused: false,\n  },\n};\n```\n\n#### ✅ **Type-Safe Extraction Patterns**\n\n**Single Value Extraction with Transformations:**\n```typescript\n// Extract and transform to number\nconst pageId = htmlParser.extractSingle\u003cnumber\u003e(\n  html, \n  '//meta[@name=\"page-id\"]', \n  'xpath', \n  'content',\n  { transform: (value: string) =\u003e parseInt(value, 10) }\n);\n\n// Extract and validate boolean\nconst isPublished = htmlParser.extractSingle\u003cboolean\u003e(\n  html,\n  '//meta[@property=\"article:published\"]',\n  'xpath',\n  'content',\n  { transform: (value: string) =\u003e value.toLowerCase() === 'true' }\n);\n\n// Extract date with validation\nconst publishedDate = htmlParser.extractSingle\u003cDate | null\u003e(\n  html,\n  '//time[@datetime]',\n  'xpath',\n  'datetime',\n  { \n    transform: (value: string) =\u003e {\n      const date = new Date(value);\n      return isNaN(date.getTime()) ? null : date;\n    }\n  }\n);\n```\n\n**Multiple Value Extraction with Type Safety:**\n```typescript\n// Extract numeric arrays with validation\nconst prices = htmlParser.extractMultiple\u003cnumber\u003e(\n  html,\n  '//span[@class=\"price\"]/text()',\n  'xpath',\n  undefined,\n  { \n    transform: (value: string) =\u003e {\n      const price = parseFloat(value.replace(/[$,]/g, ''));\n      return isNaN(price) ? 0 : price;\n    }\n  }\n);\n\n// Extract URLs with validation\nconst imageUrls = htmlParser.extractAttributes\u003cstring\u003e(\n  html,\n  '//img[@src]',\n  'src',\n  'xpath',\n  {\n    transform: (url: string) =\u003e {\n      try {\n        return new URL(url, 'https://example.com').href;\n      } catch {\n        return '';\n      }\n    }\n  }\n).filter(url =\u003e url !== '');\n```\n\n**Advanced Structured Extraction:**\n```typescript\n// Define comprehensive interfaces\ninterface Article {\n  title: string;\n  author: string;\n  publishedDate: Date | null;\n  tags: string[];\n  excerpt: string;\n  content: string;\n  wordCount: number;\n  socialShares: number;\n  isSponsored: boolean;\n  metadata: {\n    description: string;\n    keywords: string[];\n  };\n}\n\n// Create production-ready schema\nconst articleSchema: ExtractionSchema\u003cArticle\u003e = {\n  title: {\n    selector: '//h1[@class=\"article-title\"]/text() | //title/text()',\n    type: 'xpath',\n    transform: (title: string) =\u003e title.trim().replace(/\\s+/g, ' ')\n  },\n  author: {\n    selector: '//meta[@name=\"author\"]',\n    type: 'xpath',\n    attribute: 'content',\n    transform: (author: string) =\u003e author || 'Unknown'\n  },\n  publishedDate: {\n    selector: '//time[@datetime] | //meta[@property=\"article:published_time\"]',\n    type: 'xpath',\n    attribute: 'datetime',\n    transform: (dateStr: string) =\u003e {\n      if (!dateStr) return null;\n      const date = new Date(dateStr);\n      return isNaN(date.getTime()) ? null : date;\n    }\n  },\n  tags: {\n    selector: '//meta[@name=\"keywords\"]',\n    type: 'xpath',\n    attribute: 'content',\n    transform: (keywords: string) =\u003e \n      keywords ? keywords.split(',').map(k =\u003e k.trim()).filter(k =\u003e k) : []\n  },\n  excerpt: {\n    selector: '//meta[@name=\"description\"]',\n    type: 'xpath',\n    attribute: 'content',\n    transform: (desc: string) =\u003e desc || ''\n  },\n  content: {\n    selector: '//article | //div[@class=\"content\"]',\n    type: 'xpath',\n    raw: true\n  },\n  wordCount: {\n    selector: '//article//text() | //div[@class=\"content\"]//text()',\n    type: 'xpath',\n    multiple: true,\n    transform: (texts: string[]) =\u003e \n      texts.join(' ').split(/\\s+/).filter(word =\u003e word.length \u003e 0).length\n  },\n  socialShares: {\n    selector: '//span[@class=\"share-count\"]/text()',\n    type: 'xpath',\n    transform: (shares: string) =\u003e parseInt(shares?.replace(/[^0-9]/g, '') || '0', 10)\n  },\n  isSponsored: {\n    selector: '//div[contains(@class, \"sponsored\")] | //span[contains(text(), \"Sponsored\")]',\n    type: 'xpath',\n    transform: () =\u003e true\n  },\n  metadata: {\n    selector: '//head',\n    type: 'xpath',\n    transform: (headElement: any) =\u003e {\n      // Extract nested metadata\n      const description = htmlParser.extractSingle(\n        headElement,\n        '//meta[@name=\"description\"]',\n        'xpath',\n        'content'\n      ) || '';\n      \n      const keywords = htmlParser.extractSingle(\n        headElement,\n        '//meta[@name=\"keywords\"]',\n        'xpath',\n        'content'\n      ) || '';\n      \n      return {\n        description,\n        keywords: keywords.split(',').map(k =\u003e k.trim()).filter(k =\u003e k)\n      };\n    }\n  }\n};\n\nconst article: Article = htmlParser.extractStructured\u003cArticle\u003e(html, articleSchema);\n```\n\n**Advanced Transform Pipeline:**\n```typescript\n// Define reusable transform classes\nclass UppercasePipe {\n  transform(value: string): string {\n    return value.toUpperCase();\n  }\n}\n\nclass TrimPipe {\n  transform(value: string): string {\n    return value.trim().replace(/\\s+/g, ' ');\n  }\n}\n\nclass NumberPipe {\n  constructor(private defaultValue: number = 0) {}\n  \n  transform(value: string): number {\n    const num = parseFloat(value.replace(/[^0-9.-]/g, ''));\n    return isNaN(num) ? this.defaultValue : num;\n  }\n}\n\n// Use in extraction schema\nconst productSchema: ExtractionSchema\u003cany\u003e = {\n  name: {\n    selector: '//h1/text()',\n    type: 'xpath',\n    transform: [\n      TrimPipe,\n      UppercasePipe,\n      (name: string) =\u003e name.substring(0, 100) // Limit length\n    ]\n  },\n  price: {\n    selector: '//span[@class=\"price\"]/text()',\n    type: 'xpath',\n    transform: new NumberPipe(0)\n  }\n};\n```\n\n#### ⚠️ **Common Implementation Mistakes to Avoid**\n\n**Type and Method Signature Errors:**\n```typescript\n// ❌ WRONG: Missing type parameters and incorrect XPath\nconst result = htmlParser.extractSingle(html, '//wrongtag');\n\n// ✅ CORRECT: Proper type and XPath for text content\nconst result: string | null = htmlParser.extractSingle\u003cstring\u003e(html, '//title/text()');\n\n// ❌ WRONG: Using wrong method for attribute extraction\nconst urls = htmlParser.extractSingle(html, '//a', 'xpath', 'href', { multiple: true });\n\n// ✅ CORRECT: Use dedicated method for attributes\nconst urls: string[] = htmlParser.extractAttributes\u003cstring\u003e(html, '//a', 'href');\n\n// ❌ WRONG: Mixing CSS and XPath syntax\nconst links = htmlParser.extractMultiple(html, 'a//text()', 'css');\n\n// ✅ CORRECT: Use appropriate selector type\nconst links = htmlParser.extractMultiple(html, '//a/text()', 'xpath');\n// OR\nconst links = htmlParser.extractMultiple(html, 'a', 'css');\n```\n\n**Configuration and Error Handling Mistakes:**\n```typescript\n// ❌ WRONG: Missing proper response typing and error handling\nconst response = await htmlParser.fetchHtml(url);\nconst title = response.data.match(/\u003ctitle\u003e(.*?)\u003c\\/title\u003e/)?.[1];\n\n// ✅ CORRECT: Proper typing and extraction\nconst response: HtmlFetchResponse = await htmlParser.fetchHtml(url, options);\nconst title: string | null = htmlParser.extractSingle\u003cstring\u003e(\n  response.data, \n  '//title/text()'\n);\n\n// ❌ WRONG: Ignoring status codes and error types\ntry {\n  const html = await htmlParser.fetchHtml(url);\n} catch (error) {\n  console.log('Failed to fetch');\n}\n\n// ✅ CORRECT: Comprehensive error handling\ntry {\n  const response: HtmlFetchResponse = await htmlParser.fetchHtml(url, options);\n  \n  if (response.status \u003e= 400) {\n    throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n  }\n  \n  // Process response.data\n} catch (error: any) {\n  if (error.code === 'ETIMEDOUT') {\n    // Handle timeout specifically\n  } else if (error.code === 'ECONNREFUSED') {\n    // Handle connection refused\n  } else if (error.code?.includes('CERT_')) {\n    // Handle SSL certificate errors\n  } else if (error.code === 'ENOTFOUND') {\n    // Handle DNS resolution errors\n  } else {\n    // Handle other errors\n  }\n}\n```\n\n#### 🔧 **Advanced Usage Patterns**\n\n**Proxy Testing and Validation:**\n```typescript\nconst proxyConfig: ProxyConfig = {\n  url: 'http://proxy.example.com:8080',\n  type: 'http',\n  username: 'user',\n  password: 'pass'\n};\n\n// Test proxy before use\nconst isProxyWorking = await htmlParser.testProxy(proxyConfig);\nif (!isProxyWorking) {\n  throw new Error('Proxy connection failed');\n}\n\n// Use proxy for requests\nconst response = await htmlParser.fetchHtml(url, { proxy: proxyConfig });\n```\n\n**User Agent Management:**\n```typescript\n// Get random user agent for stealth scraping\nconst randomUA = await htmlParser.getRandomUserAgent();\nconsole.log('Using User Agent:', randomUA);\n\n// Use in options\nconst options: HtmlParserOptions = {\n  userAgent: randomUA,\n  // OR use built-in random generation\n  useRandomUserAgent: true\n};\n```\n\n**Conditional Extraction and Fallbacks:**\n```typescript\n// Check existence before extraction\nif (htmlParser.exists(html, '//div[@class=\"premium-content\"]')) {\n  const premiumContent = htmlParser.extractText(html, '//div[@class=\"premium-content\"]');\n} else {\n  const freeContent = htmlParser.extractText(html, '//div[@class=\"free-content\"]');\n}\n\n// Count elements for validation\nconst commentCount = htmlParser.count(html, '//div[@class=\"comment\"]');\nconsole.log(`Found ${commentCount} comments`);\n\n// Multiple selector fallback pattern\nconst title = htmlParser.extractSingle(html, '//h1/text()') ||\n              htmlParser.extractSingle(html, '//title/text()') ||\n              htmlParser.extractSingle(html, '//meta[@property=\"og:title\"]', 'xpath', 'content') ||\n              'No title found';\n```\n\n## API Reference\n\n### Core Methods\n\n#### `fetchHtml(url: string, options?: HtmlParserOptions): Promise\u003cHtmlFetchResponse\u003e`\n\nFetch HTML content from a URL with comprehensive error handling and SSL configuration.\n\n```typescript\nconst response = await htmlParser.fetchHtml('https://example.com', {\n  timeout: 10000,\n  headers: { 'User-Agent': 'Custom Agent' },\n  rejectUnauthorized: false,           // Accept self-signed certificates\n  disableServerIdentityCheck: true,    // ⚠️ Skip hostname validation (use with caution)\n  retryOnErrors: {\n    ssl: true,\n    timeout: true,\n    dns: true,\n    connectionRefused: true\n  }\n});\n```\n\n#### `extractSingle\u003cT = string\u003e(html: string, selector: string, type?: 'xpath' | 'css', attribute?: string, options?: { verbose?: boolean; transform?: (value: string) =\u003e T }): T | null`\n\nExtract a single value using XPath or CSS selector with type safety.\n\n```typescript\n// Using XPath (default)\nconst title = htmlParser.extractSingle\u003cstring\u003e(html, '//title/text()');\n\n// Using CSS selector\nconst title = htmlParser.extractSingle\u003cstring\u003e(html, 'title', 'css');\n\n// Extract attribute with transformation\nconst id = htmlParser.extractSingle\u003cnumber\u003e(html, '//div[@data-id]', 'xpath', 'data-id', {\n  transform: (value: string) =\u003e parseInt(value)\n});\n\n// Extract with boolean transformation\nconst isActive = htmlParser.extractSingle\u003cboolean\u003e(html, '//div/@data-active', 'xpath', undefined, {\n  transform: (value: string) =\u003e value === 'true'\n});\n```\n\n#### `extractMultiple\u003cT = string\u003e(html: string, selector: string, type?: 'xpath' | 'css', attribute?: string, options?: { verbose?: boolean; transform?: (value: string) =\u003e T }): T[]`\n\nExtract multiple matching values with type safety.\n\n```typescript\n// Extract all links\nconst links = htmlParser.extractMultiple\u003cstring\u003e(html, '//a/text()');\n\n// Extract all href attributes\nconst urls = htmlParser.extractMultiple\u003cstring\u003e(html, '//a', 'xpath', 'href');\n\n// Extract with transformation\nconst prices = htmlParser.extractMultiple\u003cnumber\u003e(html, '//span[@class=\"price\"]/text()', 'xpath', undefined, {\n  transform: (value: string) =\u003e parseFloat(value.replace('$', ''))\n});\n```\n\n#### `extractText\u003cT = string\u003e(html: string, selector: string, type?: 'xpath' | 'css', options?: { verbose?: boolean; transform?: (value: string) =\u003e T }): T | null`\n\nExtract text content specifically with type safety.\n\n```typescript\nconst text = htmlParser.extractText\u003cstring\u003e(html, '//p[@class=\"content\"]');\n\n// Extract with transformation\nconst wordCount = htmlParser.extractText\u003cnumber\u003e(html, '//p[@class=\"content\"]', 'xpath', {\n  transform: (text: string) =\u003e text.split(' ').length\n});\n```\n\n#### `extractAttributes\u003cT = string\u003e(html: string, selector: string, attribute: string, type?: 'xpath' | 'css', options?: { verbose?: boolean; transform?: (value: string) =\u003e T }): T[]`\n\nExtract attribute values from multiple elements with type safety.\n\n```typescript\nconst imgSources = htmlParser.extractAttributes\u003cstring\u003e(html, '//img', 'src');\n\n// Extract with transformation\nconst ids = htmlParser.extractAttributes\u003cnumber\u003e(html, '//div', 'data-id', 'xpath', {\n  transform: (value: string) =\u003e parseInt(value)\n});\n```\n\n#### `exists(html: string, selector: string, type?: 'xpath' | 'css'): boolean`\n\nCheck if elements exist.\n\n```typescript\nconst hasComments = htmlParser.exists(html, '//div[@class=\"comments\"]');\n```\n\n#### `count(html: string, selector: string, type?: 'xpath' | 'css'): number`\n\nCount matching elements.\n\n```typescript\nconst commentCount = htmlParser.count(html, '//div[@class=\"comment\"]');\n```\n\n#### `getRandomUserAgent(): Promise\u003cstring\u003e`\n\nGenerate a random user agent string.\n\n```typescript\nconst randomUA = await htmlParser.getRandomUserAgent();\nconsole.log(randomUA);\n// Output: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36...\n```\n\n#### `testProxy(proxy: ProxyConfig, testUrl?: string): Promise\u003cboolean\u003e`\n\nTest if a proxy connection is working.\n\n```typescript\nconst proxyConfig = {\n  url: 'http://proxy.example.com:8080',\n  type: 'http' as const,\n  username: 'user',\n  password: 'pass'\n};\n\nconst isWorking = await htmlParser.testProxy(proxyConfig);\nconsole.log(`Proxy is ${isWorking ? 'working' : 'not working'}`);\n```\n\n#### `transform` option in schema fields\n\nThe `transform` property in a schema field is highly flexible. You can use:\n- A single function: `(value: string) =\u003e any`\n- A single class (constructor with a `transform` method): `class MyPipe { transform(value) { ... } }` (the parser will instantiate it automatically)\n- A single instance (object with a `transform` method): `new MyPipe()`\n- A class constructor with a `transform` method (e.g., `MyPipe`)\n- An array of any of the above (functions, classes, instances), which will be applied in order\n\n**Note:** The parser will always convert DOM elements to their text content before applying the transform, so your transform functions can safely expect a string.\n\n**Important:**\n- If you use a class or object for `transform`, it **must** have a method named `transform(value)`. The parser will call this method with the extracted value.\n- Custom class transforms must also have a constructor method (either a default constructor or one that accepts arguments if you instantiate it yourself). The parser will instantiate the class using its constructor if you pass the class itself (not an instance).\n\n**Example of a valid custom class transform:**\n```typescript\nclass MyCustomPipe {\n  // Default constructor\n  constructor() {}\n  transform(value: string) {\n    // your transformation logic\n    return value + '!';\n  }\n}\n\nclass SuffixPipe {\n  constructor(private suffix: string) {}\n  transform(value: string) {\n    return value + this.suffix;\n  }\n}\n\n// Usage:\ntransform: MyCustomPipe\n// or\ntransform: new MyCustomPipe()\n```\n\n**Examples:**\n```typescript\n// Single function\ntransform: (value: string) =\u003e value.toUpperCase()\n\n// Single class\ntransform: UppercasePipe\n\n// Single instance\ntransform: new SuffixPipe('!')\n\n// Array of functions\ntransform: [\n  (value: string) =\u003e value.trim(),\n  (value: string) =\u003e value.toUpperCase(),\n]\n\n// Array of classes and/or instances and/or functions\ntransform: [\n  (value: string) =\u003e value.trim(),\n  UppercasePipe,\n  new SuffixPipe(' [ADVANCED]'),\n]\n```\n\n### Advanced Methods\n\n#### `extractStructured\u003cT = Record\u003cstring, any\u003e\u003e(html: string, schema: ExtractionSchema\u003cT\u003e, options?: { verbose?: boolean }): T`\n\nExtract data using a typed schema object. Supports `multiple: true` for array extraction and `raw: true` for raw HTML extraction in any field.\n\n```typescript\nimport { ExtractionSchema } from '@hanivanrizky/nestjs-html-parser';\n\n// Define typed interface\ninterface Article {\n  title: string;\n  author: string;\n  links: string[];\n  titleHtml: string;\n}\n\n// Create typed schema\nconst schema: ExtractionSchema\u003cArticle\u003e = {\n  title: {\n    selector: '//title/text()',\n    type: 'xpath'\n  },\n  author: {\n    selector: '//meta[@name=\"author\"]',\n    type: 'xpath',\n    attribute: 'content'\n  },\n  links: {\n    selector: '//a/@href',\n    type: 'xpath',\n    multiple: true\n  },\n  titleHtml: {\n    selector: '//title',\n    type: 'xpath',\n    raw: true\n  }\n};\n\nconst result = htmlParser.extractStructured\u003cArticle\u003e(html, schema);\n// Result: { title: \"Page Title\", author: \"John Doe\", links: [\"/home\", \"/about\", ...], titleHtml: \"\u003ctitle\u003ePage Title\u003c/title\u003e\" }\n```\n\n#### `extractStructuredList\u003cT = Record\u003cstring, any\u003e\u003e(html: string, containerSelector: string, schema: ExtractionSchema\u003cT\u003e, containerType?: 'xpath' | 'css', options?: { verbose?: boolean }): T[]`\n\nExtract arrays of typed structured data. Supports `multiple: true` for array extraction and `raw: true` for raw HTML extraction in any field.\n\n```typescript\n// Define typed interface\ninterface Product {\n  name: string;\n  price: number;\n  tags: string[];\n  nameHtml: string;\n}\n\n// Create typed schema\nconst productSchema: ExtractionSchema\u003cProduct\u003e = {\n  name: {\n    selector: './/h2/text()',\n    type: 'xpath'\n  },\n  price: {\n    selector: './/span[@class=\"price\"]/text()',\n    type: 'xpath',\n    transform: (value: string) =\u003e parseFloat(value.replace('$', ''))\n  },\n  tags: {\n    selector: './/span[@class=\"tag\"]/text()',\n    type: 'xpath',\n    multiple: true\n  },\n  nameHtml: {\n    selector: './/h2',\n    type: 'xpath',\n    raw: true\n  }\n};\n\nconst products = htmlParser.extractStructuredList\u003cProduct\u003e(\n  html,\n  '//div[@class=\"product\"]',\n  productSchema\n);\n// Result: Product[] with tags as array and nameHtml as raw HTML for each product\n// [\n//   { name: \"Product A\", price: 19.99, tags: [\"electronics\", \"gadget\"], nameHtml: \"\u003ch2\u003eProduct A\u003c/h2\u003e\" },\n//   { name: \"Product B\", price: 29.99, tags: [\"accessory\"], nameHtml: \"\u003ch2\u003eProduct B\u003c/h2\u003e\" }\n// ]\n```\n\n## Development\n\n```bash\n# Install dependencies\nyarn install\n\n# Build\nyarn build\n\n# Test\nyarn test\nyarn test:cov\nyarn test:watch\n```\n\n## Contributing\n\n1. Fork the repository\n2. Create your feature branch (`git checkout -b feature/yourusername/amazing-feature`)\n3. Commit your changes (`git commit -m 'Add some amazing feature'`)\n4. Push to the branch (`git push origin feature/yourusername/amazing-feature`)\n5. Open a Pull Request\n\n## License\n\nThis project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhanivan%2Fnestjs-html-parser","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fhanivan%2Fnestjs-html-parser","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fhanivan%2Fnestjs-html-parser/lists"}