@involvex/bun-crawl 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,64 @@
1
+ import type {FetcherOptions} from '../types'
2
+
3
+ export class Fetcher {
4
+ private readonly defaultOptions: Required<FetcherOptions>
5
+
6
+ constructor(options: FetcherOptions = {}) {
7
+ this.defaultOptions = {
8
+ timeout: options.timeout ?? 30000,
9
+ userAgent: options.userAgent ?? 'bun-crawl/0.1.0',
10
+ headers: options.headers ?? {},
11
+ followRedirects: options.followRedirects ?? true,
12
+ }
13
+ }
14
+
15
+ async fetch(
16
+ url: string,
17
+ ): Promise<{html: string; url: string; status: number}> {
18
+ const controller = new AbortController()
19
+ const timeoutId = setTimeout(
20
+ () => controller.abort(),
21
+ this.defaultOptions.timeout,
22
+ )
23
+
24
+ try {
25
+ const response = await fetch(url, {
26
+ method: 'GET',
27
+ headers: {
28
+ 'User-Agent': this.defaultOptions.userAgent,
29
+ Accept:
30
+ 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
31
+ 'Accept-Language': 'en-US,en;q=0.5',
32
+ ...this.defaultOptions.headers,
33
+ },
34
+ redirect: this.defaultOptions.followRedirects ? 'follow' : 'manual',
35
+ signal: controller.signal,
36
+ })
37
+
38
+ clearTimeout(timeoutId)
39
+
40
+ const html = await response.text()
41
+
42
+ return {
43
+ html,
44
+ url: response.url,
45
+ status: response.status,
46
+ }
47
+ } catch (error) {
48
+ clearTimeout(timeoutId)
49
+
50
+ if (error instanceof Error && error.name === 'AbortError') {
51
+ throw new Error(
52
+ `Request timeout after ${this.defaultOptions.timeout}ms: ${url}`,
53
+ {cause: error},
54
+ )
55
+ }
56
+
57
+ const caughtError =
58
+ error instanceof Error ? error : new Error(String(error))
59
+ throw new Error(`Failed to fetch ${url}: ${caughtError.message}`, {
60
+ cause: caughtError,
61
+ })
62
+ }
63
+ }
64
+ }
@@ -0,0 +1,108 @@
1
+ import TurndownService from 'turndown'
2
+
3
+ export interface MarkdownOptions {
4
+ headingStyle?: 'setext' | 'atx'
5
+ codeBlockStyle?: 'fenced' | 'indented'
6
+ bulletListMarker?: '-' | '*' | '+'
7
+ emDelimiter?: '_' | '*'
8
+ strongDelimiter?: '__' | '**'
9
+ linkStyle?: 'inlined' | 'referenced'
10
+ linkReferenceStyle?: 'full' | 'collapsed' | 'shortcut'
11
+ }
12
+
13
+ export class MarkdownConverter {
14
+ private readonly turndown: TurndownService
15
+
16
+ constructor(options: MarkdownOptions = {}) {
17
+ this.turndown = new TurndownService({
18
+ headingStyle: options.headingStyle ?? 'atx',
19
+ codeBlockStyle: options.codeBlockStyle ?? 'fenced',
20
+ bulletListMarker: options.bulletListMarker ?? '-',
21
+ emDelimiter: options.emDelimiter ?? '_',
22
+ strongDelimiter: options.strongDelimiter ?? '**',
23
+ linkStyle: options.linkStyle ?? 'inlined',
24
+ linkReferenceStyle: options.linkReferenceStyle ?? 'full',
25
+ })
26
+
27
+ this.configureRules()
28
+ }
29
+
30
+ private configureRules(): void {
31
+ this.turndown.addRule('strikeThrough', {
32
+ filter: ['del', 's'],
33
+ replacement: content => `~~${content}~~`,
34
+ })
35
+
36
+ this.turndown.addRule('underline', {
37
+ filter: 'u',
38
+ replacement: content => `<u>${content}</u>`,
39
+ })
40
+
41
+ this.turndown.addRule('subscript', {
42
+ filter: 'sub',
43
+ replacement: content => `~${content}~`,
44
+ })
45
+
46
+ this.turndown.addRule('superscript', {
47
+ filter: 'sup',
48
+ replacement: content => `^${content}^`,
49
+ })
50
+
51
+ this.turndown.addRule('details', {
52
+ filter: 'details',
53
+ replacement: (content, node) => {
54
+ const summary = node.querySelector('summary')?.textContent || 'Details'
55
+ return `\n<details>\n<summary>${summary}</summary>\n${content}\n</details>\n`
56
+ },
57
+ })
58
+
59
+ this.turndown.addRule('taskListItems', {
60
+ filter: node =>
61
+ node.nodeName === 'INPUT' && node.getAttribute('type') === 'checkbox',
62
+ replacement: (_, node) => {
63
+ const checked = node.hasAttribute('checked') ? 'x' : ' '
64
+ return `- [${checked}] `
65
+ },
66
+ })
67
+
68
+ this.turndown.addRule('table', {
69
+ filter: 'table',
70
+ replacement: (content, _node) => {
71
+ return `\n${content}\n`
72
+ },
73
+ })
74
+
75
+ this.turndown.addRule('tableRow', {
76
+ filter: 'tr',
77
+ replacement: (content, node) => {
78
+ const isHeader = node.parentElement?.nodeName === 'THEAD'
79
+ const cells = node.querySelectorAll('th, td')
80
+ const cellContents = Array.from(cells).map(
81
+ (cell: Element) =>
82
+ cell.textContent?.replace(/\|/g, '\\|').trim() || '',
83
+ )
84
+ const separator = isHeader
85
+ ? cellContents.map(() => '---').join('|')
86
+ : ''
87
+ return `| ${cellContents.join(' | ')} |\n${separator ? `| ${separator} |\n` : ''}`
88
+ },
89
+ })
90
+
91
+ this.turndown.addRule('pre', {
92
+ filter: 'pre',
93
+ replacement: (content, node) => {
94
+ const code = node.querySelector('code')
95
+ const language = code?.className?.match(/language-(\w+)/)?.[1] || ''
96
+ return `\n\`\`\`${language}\n${content}\n\`\`\`\n`
97
+ },
98
+ })
99
+ }
100
+
101
+ convert(html: string): string {
102
+ return this.turndown.turndown(html).trim()
103
+ }
104
+
105
+ getTurndownService(): TurndownService {
106
+ return this.turndown
107
+ }
108
+ }
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ export type {AIExtractor, AIExtractorConfig} from './ai/interface'
2
+ export {NullAIExtractor} from './ai/null-ai'
3
+ export {ZenAIExtractor} from './ai/zen-ai'
4
+ export {Cleaner, type CleanerResult} from './core/cleaner'
5
+ export {Crawler} from './core/crawler'
6
+ export {Fetcher} from './core/fetcher'
7
+ export {MarkdownConverter, type MarkdownOptions} from './core/markdown'
8
+ export type {
9
+ CrawlOptions,
10
+ CrawlResult,
11
+ FetcherOptions,
12
+ ScrapeResult,
13
+ } from './types'
package/src/types.ts ADDED
@@ -0,0 +1,54 @@
1
+ export interface ScrapeResult {
2
+ url: string
3
+ title: string
4
+ markdown: string
5
+ html: string
6
+ metadata: {
7
+ description?: string
8
+ author?: string
9
+ publishedTime?: string
10
+ [key: string]: unknown
11
+ }
12
+ }
13
+
14
+ export interface CrawlOptions {
15
+ depth: number
16
+ limit: number
17
+ sameDomain: boolean
18
+ concurrency: number
19
+ }
20
+
21
+ export interface CrawlResult {
22
+ pages: ScrapeResult[]
23
+ errors: Array<{url: string; error: string}>
24
+ stats: {
25
+ total: number
26
+ succeeded: number
27
+ failed: number
28
+ }
29
+ }
30
+
31
+ export interface FetcherOptions {
32
+ timeout?: number
33
+ userAgent?: string
34
+ headers?: Record<string, string>
35
+ followRedirects?: boolean
36
+ }
37
+
38
+ export interface AIExtractorConfig {
39
+ enabled?: boolean
40
+ }
41
+
42
+ export interface CliOptions {
43
+ [key: string]: string | boolean | undefined
44
+ output?: string | boolean
45
+ format?: string | boolean
46
+ depth?: string | boolean
47
+ limit?: string | boolean
48
+ outDir?: string | boolean
49
+ sameDomain?: boolean
50
+ concurrency?: string | boolean
51
+ timeout?: string | boolean
52
+ userAgent?: string | boolean
53
+ json?: boolean
54
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "lib": ["ES2022", "DOM"],
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "outDir": "./dist",
14
+ "noEmit": true,
15
+ "allowImportingTsExtensions": true,
16
+ "verbatimModuleSyntax": true,
17
+ "moduleDetection": "force",
18
+ "types": ["bun"]
19
+ },
20
+ "include": ["src/**/*"],
21
+ "exclude": ["node_modules", "dist"]
22
+ }