Files
IT Dog 0d3e40f797 v1.0.0: URL-to-PDF + Doc Site Crawler
- Single URL to PDF via Puppeteer (Chrome headless)
- Doc Site Crawler: crawl entire documentation sites
  - Auto-extract all pages from TOC navigation
  - 20-page batch processing with browser restart (anti-OOM)
  - Output: PDF + Markdown
- Flask-style HTTP server on port 8099
- Deployed at https://pdf.donton.cloud/url2pdf
2026-05-22 14:24:05 +08:00

123 lines
4.6 KiB
JavaScript

// Crawl Trend Micro docs and convert to a single Markdown file
const puppeteer = require("puppeteer");
const fs = require("fs");
const BASE = process.argv[2];
const OUT = "/tmp/apex_one_admin_guide.md";
const baseUrl = new URL(BASE);
const articleIdx = baseUrl.pathname.lastIndexOf("/");
const articleDir = baseUrl.pathname.substring(0, articleIdx + 1);
(async () => {
const browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
const mainPage = await browser.newPage();
console.log("🌐 Loading TOC:", BASE);
await mainPage.goto(BASE, { waitUntil: "networkidle2", timeout: 30000 });
await new Promise(r => setTimeout(r, 3000));
// Extract all doc links
const allLinks = await mainPage.evaluate(() => {
const links = [];
document.querySelectorAll("a[href]").forEach(a => {
const text = a.textContent.trim();
if (text.length > 2) links.push({ href: a.href, text: text.slice(0, 80) });
});
return links;
});
await mainPage.close();
// Filter: same article dir, en-us only, unique filenames (same logic as PDF crawler)
const seenFiles = new Set();
const docPages = [];
for (const link of allLinks) {
try {
const u = new URL(link.href);
if (u.hostname !== baseUrl.hostname) continue;
if (!u.pathname.startsWith(articleDir)) continue;
if (!u.pathname.startsWith("/en-us/")) continue;
if (u.hash) continue;
const fn = u.pathname.split("/").pop();
if (!fn || fn.length < 20) continue;
const lower = link.text.toLowerCase();
if (/english|dansk|deutsch|español|français|italiano|nederlands|norsk|polski|português|svenska|ภาษาไทย|tiếng việt|türkçe|čeština|ελληνικά|български|русский|עברית|العربية|日本語|简体中文|繁體中文|한국어|bahasa indonesia/i.test(lower)) continue;
if (/online help center|security for endpoints|apex one$/i.test(lower)) continue;
if (!seenFiles.has(fn)) {
seenFiles.add(fn);
docPages.push({ href: link.href, text: link.text });
}
} catch(e) {}
}
console.log(`📋 ${docPages.length} pages to extract`);
const stream = fs.createWriteStream(OUT);
stream.write("# Apex One Service Pack 1 (2025) — Admin Guide\n\n");
stream.write(`> Crawled from ${BASE}\n\n`);
stream.write("---\n\n");
for (let i = 0; i < docPages.length; i++) {
const { href, text } = docPages[i];
try {
const page = await browser.newPage();
await page.goto(href, { waitUntil: "networkidle2", timeout: 20000 });
await new Promise(r => setTimeout(r, 500));
// Extract title + body content
const content = await page.evaluate(() => {
// Try to find the main content area
const mainSel = ["article", ".content", ".main-content", ".doc-content", "main", "#content", ".article-content"];
let main = null;
for (const sel of mainSel) {
main = document.querySelector(sel);
if (main) break;
}
if (!main) main = document.body;
// Get H1 first
const h1 = main.querySelector("h1, h2");
const title = h1 ? h1.textContent.trim() : document.title.replace(" | TrendAI™", "").trim();
// Get all text content, preserving some structure
const blocks = [];
main.querySelectorAll("h1, h2, h3, h4, p, li, table, pre, .note, .warning, .tip").forEach(el => {
const tag = el.tagName;
const txt = el.textContent.trim();
if (!txt) return;
if (tag.match(/^H[1-4]$/)) {
const level = parseInt(tag[1]);
blocks.push("\n" + "#".repeat(level + 1) + " " + txt + "\n");
} else if (tag === "LI") {
blocks.push("- " + txt);
} else if (tag === "PRE") {
blocks.push("\n```\n" + txt + "\n```\n");
} else if (tag === "TABLE") {
blocks.push("\n<!-- table omitted -->\n");
} else {
blocks.push(txt);
}
});
return { title, body: blocks.join("\n\n") };
});
await page.close();
stream.write(`## ${i + 1}. ${content.title}\n\n`);
stream.write(content.body || "_No content extracted_\n");
stream.write("\n---\n\n");
if ((i + 1) % 50 === 0) console.log(` ${i + 1}/${docPages.length}`);
} catch(e) {
console.log(` ⚠️ [${i+1}] ${text.slice(0,40)}: ${e.message.slice(0,50)}`);
stream.write(`## ${i + 1}. ${text}\n\n_Error: ${e.message}_\n\n---\n\n`);
}
}
stream.end();
await browser.close();
const stats = fs.statSync(OUT);
console.log(`\n${OUT} (${(stats.size/1024).toFixed(0)}KB)`);
})();