import fs from "fs"; import { marked } from "marked"; import puppeteer from "puppeteer-core"; // ---- Locate a Chrome/Chromium/Edge binary for PDF rendering ---- export function findChrome() { const candidates = [ process.env.CHROME_PATH, "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary", "/Applications/Chromium.app/Contents/MacOS/Chromium", "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge", "/usr/bin/google-chrome", "/usr/bin/chromium-browser", "/usr/bin/chromium", ].filter(Boolean); return candidates.find((p) => { try { return fs.existsSync(p); } catch { return false; } }); } // ---- Safe file name for downloads / zip entries ---- export function safeName(s, fallback = "not") { const clean = String(s || "") .replace(/[\/\\?%*:|"<>]/g, "-") .replace(/\s+/g, " ") .trim(); return clean || fallback; } // Build "Parent/Child" folder path for a category id export function categoryPath(catId, flatCats) { const byId = new Map(flatCats.map((c) => [c.id, c])); const parts = []; let cur = byId.get(catId); let guard = 0; while (cur && guard++ < 10) { parts.unshift(safeName(cur.name)); cur = cur.parent_id ? byId.get(cur.parent_id) : null; } return parts.join("/"); } // ---- One note -> Markdown ---- export function noteToMarkdown(note, path = "") { const lines = [`# ${note.title}`, ""]; const meta = []; if (path) meta.push(`**Klasör:** ${path}`); if (note.pinned) meta.push("**📌 Sabitlenmiş**"); meta.push(`**Güncelleme:** ${fmt(note.updated_at)}`); lines.push(meta.join(" \n"), "", "---", ""); lines.push(note.content || "_(boş)_", ""); return lines.join("\n") + "\n"; } function fmt(d) { if (!d) return "-"; return new Date(d.replace(" ", "T")).toLocaleString("tr-TR", { day: "2-digit", month: "long", year: "numeric", hour: "2-digit", minute: "2-digit", }); } function escapeHtml(s) { return String(s ?? "").replace(/&/g, "&").replace(//g, ">"); } // ---- Notes -> printable HTML (markdown content rendered) ---- export function notesToHtml(notes, pathFn, title = "Bilgi Merkezi") { const body = notes .map((n) => { const path = pathFn(n.category_id); return `

${escapeHtml(n.title)}${n.pinned ? ' 📌' : ""}

${path ? "📁 " + escapeHtml(path) + " · " : ""}Güncelleme: ${escapeHtml(fmt(n.updated_at))}
${marked.parse(n.content || "_(boş)_")}
`; }) .join("\n"); return `

${escapeHtml(title)}

${notes.length} not · ${new Date().toLocaleDateString("tr-TR", { day: "2-digit", month: "long", year: "numeric" })}
${body || "

Not bulunamadı.

"} `; } // ---- Minimal dependency-free ZIP (stored / no compression) ---- const CRC_TABLE = (() => { const t = new Uint32Array(256); for (let n = 0; n < 256; n++) { let c = n; for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; t[n] = c >>> 0; } return t; })(); function crc32(buf) { let c = 0xffffffff; for (let i = 0; i < buf.length; i++) c = CRC_TABLE[(c ^ buf[i]) & 0xff] ^ (c >>> 8); return (c ^ 0xffffffff) >>> 0; } // files: [{ name, data: string|Buffer }] -> Buffer (valid .zip, UTF-8 names) export function makeZip(files) { const parts = []; const central = []; let offset = 0; for (const f of files) { const nameBuf = Buffer.from(f.name, "utf8"); const data = Buffer.isBuffer(f.data) ? f.data : Buffer.from(f.data, "utf8"); const crc = crc32(data); const local = Buffer.alloc(30); local.writeUInt32LE(0x04034b50, 0); local.writeUInt16LE(20, 4); local.writeUInt16LE(0x0800, 6); // UTF-8 flag local.writeUInt16LE(0, 8); // store local.writeUInt32LE(0, 10); // time+date local.writeUInt32LE(crc, 14); local.writeUInt32LE(data.length, 18); local.writeUInt32LE(data.length, 22); local.writeUInt16LE(nameBuf.length, 26); local.writeUInt16LE(0, 28); parts.push(local, nameBuf, data); const cd = Buffer.alloc(46); cd.writeUInt32LE(0x02014b50, 0); cd.writeUInt16LE(20, 4); cd.writeUInt16LE(20, 6); cd.writeUInt16LE(0x0800, 8); cd.writeUInt16LE(0, 10); cd.writeUInt32LE(0, 12); cd.writeUInt32LE(crc, 16); cd.writeUInt32LE(data.length, 20); cd.writeUInt32LE(data.length, 24); cd.writeUInt16LE(nameBuf.length, 28); cd.writeUInt32LE(0, 30); // extra+comment len cd.writeUInt16LE(0, 34); // disk cd.writeUInt16LE(0, 36); // internal attr cd.writeUInt32LE(0, 38); // external attr cd.writeUInt32LE(offset, 42); central.push(cd, nameBuf); offset += local.length + nameBuf.length + data.length; } const centralBuf = Buffer.concat(central); const end = Buffer.alloc(22); end.writeUInt32LE(0x06054b50, 0); end.writeUInt16LE(files.length, 8); end.writeUInt16LE(files.length, 10); end.writeUInt32LE(centralBuf.length, 12); end.writeUInt32LE(offset, 16); return Buffer.concat([...parts, centralBuf, end]); } // ---- HTML -> PDF buffer ---- export async function htmlToPdf(html) { const executablePath = findChrome(); if (!executablePath) { throw new Error("PDF için Chrome bulunamadı. .env içine CHROME_PATH ekleyin."); } const browser = await puppeteer.launch({ executablePath, headless: "new", args: ["--no-sandbox", "--disable-setuid-sandbox", "--disable-dev-shm-usage"], }); try { const page = await browser.newPage(); await page.setContent(html, { waitUntil: "networkidle0" }); return await page.pdf({ format: "A4", printBackground: true, margin: { top: "16mm", bottom: "16mm", left: "14mm", right: "14mm" }, }); } finally { await browser.close(); } }