{"version":3,"sources":["../src/index.ts"],"sourcesContent":["import puppeteer, { Page, Browser } from \"puppeteer\";\n\ntype SEARCH_ENGINE = \"google\";\n\nexport class MarkdownCrawler {\n    browser: Browser | undefined;\n    headless: boolean = false;\n    constructor(_headless: boolean) {\n        this.headless = _headless;\n    }\n\n    async crawlFromUrl(\n        url: string,\n    ): Promise<{ markdown: string; status: number }> {\n        if (!this.isValidUrl(url)) {\n            throw new Error(\n                \"Invalid URL provided, should be a full URL starting with http:// or https://\",\n            );\n        }\n\n        if (!(await this.ensureBrowser())) {\n            throw new Error(\"Could not start browser instance\");\n        }\n\n        return this.crawlSingleWebsite(url);\n    }\n    async crawlFromKeyword(\n        keyword: string,\n        search_engine: SEARCH_ENGINE,\n    ): Promise<{ markdown: string; url: string }[]> {\n        if (typeof keyword !== \"string\") {\n            throw new Error(\"Invalid Keyword provided, should be a string\");\n        }\n\n        if (search_engine !== \"google\") {\n            throw new Error(\n                \"only google search engine is supported at the moment\",\n            );\n        }\n        if (!(await this.ensureBrowser())) {\n            throw new Error(\"Could not start browser instance\");\n        }\n\n        const urls = await this.crawlSearchEngine(keyword);\n        if (urls.length > 0) {\n            return await this.crawllMultipleWebsite(urls);\n        } else {\n            throw new Error(\"Couldn't crawl for this keyword\");\n        }\n    }\n\n    async ensureBrowser():Promise<boolean> {\n        let retries = 3;\n        while (retries) {\n            if (!this.browser || !this.browser.connected) {\n                try {\n                    this.browser = await puppeteer.launch({ headless: this.headless });\n                    return true;\n                } catch (e) {\n                    console.error(\n                        \"Browser DO: Could not start browser instance. Error: ${e}\",\n                    );\n                    retries--;\n                    if (!retries) {\n                        return false;\n                    }\n\n                    console.log(\n                        \"Retrying to start browser instance. Retries left: ${retries}\",\n                    );\n                }\n            } else {\n                return true;\n            }\n        }\n        return false;\n    }\n    async crawlSearchEngine(keyword: string):Promise<string[]> {\n        const page = await this.browser!.newPage();\n        await page.goto(\"https://www.google.com\");\n        await page.type(\"textarea[name=q]\", keyword);\n        await Promise.all([\n            page.waitForNavigation(),\n            await page.keyboard.press(\"Enter\"), // Action that causes navigation\n        ]);\n\n        // Function to scroll the page\n        const scrollToBottom = async ():Promise<void> => {\n            await page.evaluate(async () => {\n                // Scroll down to the bottom of the page\n                window.scrollTo(0, document.body.scrollHeight);\n                // Wait for new content to load\n                await new Promise((resolve) => setTimeout(resolve, 2000));\n            });\n        };\n\n        let previousHeight;\n        let currentHeight = await page.evaluate(\n            () => document.body.scrollHeight,\n        );\n\n        while (true) {\n            previousHeight = currentHeight;\n            await scrollToBottom();\n            currentHeight = await page.evaluate(\n                () => document.body.scrollHeight,\n            );\n\n            // Break if we reached the bottom of the page\n            if (currentHeight === previousHeight) {\n                break;\n            }\n        }\n\n        const searchResults = await page.evaluate(() => {\n            const results = [];\n            const items = document.querySelectorAll(\"h3\");\n            for (let i = 0; i < items.length; i++) {\n                const titleElement = items[i];\n                if (titleElement) {\n                    const link = titleElement.closest(\"a\");\n                    results.push({\n                        title: titleElement.innerText,\n                        url: link ? link.href : \"\",\n                    });\n                }\n            }\n            return results;\n        });\n        const urls = searchResults.map((search) => search.url);\n        return urls;\n    }\n    async crawllMultipleWebsite(urls: string[]) : Promise<{url: string;markdown: string;}[]>{\n        const mds = [];\n        for (let index = 0; index < urls.length; index++) {\n            const url = urls[index];\n            try {\n                const markdown = await this.fetchAndProcessPage(url);\n                mds.push({ url, markdown });\n                console.log(`${index + 1}/${urls.length} done, url : ${url}`);\n            } catch (error) {\n                console.error(\n                    `${index + 1}/${urls.length} failed, url : ${url}`,\n                );\n            }\n        }\n        await this.browser?.close();\n        return mds;\n    }\n    async crawlSingleWebsite(url: string) : Promise<{markdown: string;status: number;}>{\n        const md = await this.getWebsiteMarkdown({\n            urls: [url],\n        });\n        await this.browser?.close();\n        return {\n            markdown: md[0]?.markdown,\n            status: 200,\n        };\n    }\n\n    async extractLinks(page: Page, baseUrl: string): Promise<string[]> {\n        return await page.evaluate((baseUrl) => {\n            return Array.from(document.querySelectorAll(\"a\"))\n                .map((link) => (link as { href: string }).href)\n                .filter((link) => link.startsWith(baseUrl));\n        }, baseUrl);\n    }\n\n    async getWebsiteMarkdown({ urls }: { urls: string[] }): Promise<{\n        url: string;\n        markdown: string;\n    }[]> {\n        const isBrowserActive = await this.ensureBrowser();\n\n        if (!isBrowserActive) {\n            throw new Error(\"Could not start browser instance\");\n        }\n\n        return await Promise.all(\n            urls.map(async (url) => {\n                try {\n                    const markdown = await this.fetchAndProcessPage(url);\n                    return { url, markdown };\n                } catch (error) {\n                    console.error(error);\n                    return { url, markdown: \"\" };\n                }\n            }),\n        );\n    }\n\n    async fetchAndProcessPage(url: string): Promise<string> {\n        const page = await this.browser!.newPage();\n        await page.goto(url, { waitUntil: \"networkidle0\" });\n        const md = await page.evaluate(() => {\n            function extractPageMarkdown():string {\n                const readabilityScript = document.createElement(\"script\");\n                readabilityScript.src =\n                    \"https://unpkg.com/@mozilla/readability/Readability.js\";\n                document.head.appendChild(readabilityScript);\n\n                const turndownScript = document.createElement(\"script\");\n                turndownScript.src =\n                    \"https://unpkg.com/turndown/dist/turndown.js\";\n                document.head.appendChild(turndownScript);\n\n                let md = \"no content\";\n\n                // Wait for the libraries to load\n                md = Promise.all([\n                    new Promise(\n                        (resolve) => (readabilityScript.onload = resolve),\n                    ),\n                    new Promise((resolve) => (turndownScript.onload = resolve)),\n                ]).then(() => {\n                    // Turndown instance to convert HTML to Markdown\n                    // @ts-ignore\n                    const turndownService = new TurndownService();\n\n                    const documentWithoutScripts = document.cloneNode(true);\n                    /*if (!(documentWithoutScripts instanceof Element)) {\n                        throw new Error('documentNode is not an Element');\n                    }*/\n                    // @ts-ignore\n                    documentWithoutScripts.querySelectorAll(\"script\")\n                        .forEach((browserItem: any) => browserItem.remove());\n                    // @ts-ignore\n                    documentWithoutScripts.querySelectorAll(\"style\").forEach((browserItem: any) => browserItem.remove());\n                    // @ts-ignore\n                    documentWithoutScripts.querySelectorAll(\"iframe\").forEach((browserItem: any) => browserItem.remove());\n                    // @ts-ignore\n                    documentWithoutScripts.querySelectorAll(\"noscript\").forEach((browserItem: any) => browserItem.remove());\n\n                    // article content to Markdown\n                    const markdown = turndownService.turndown(\n                        documentWithoutScripts,\n                    );\n\n                    return markdown;\n                }) as unknown as string;\n\n                return md;\n            }\n            return extractPageMarkdown();\n        });\n        await page.close();\n        return md;\n    }\n    isValidUrl(url: string): boolean {\n        return /^(http|https):\\/\\/[^ \"]+$/.test(url);\n    }\n}\n\nasync function test(){\n    const url = \"http://example.com/\"\n    const crawler = new MarkdownCrawler(true)\n    const result = await crawler.crawlFromUrl(url)\n    console.log(result);\n    \n}\ntest()"],"mappings":"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","names":["puppeteer","baseUrl","md"]}