| import fs from "fs"; |
| import { marked } from "marked"; |
| import puppeteer from "puppeteer-core"; |
|
|
| |
| 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; } |
| }); |
| } |
|
|
| |
| export function safeName(s, fallback = "not") { |
| const clean = String(s || "") |
| .replace(/[\/\\?%*:|"<>]/g, "-") |
| .replace(/\s+/g, " ") |
| .trim(); |
| return clean || fallback; |
| } |
|
|
| |
| 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("/"); |
| } |
|
|
| |
| 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, "<").replace(/>/g, ">"); |
| } |
|
|
| |
| export function notesToHtml(notes, pathFn, title = "Bilgi Merkezi") { |
| const body = notes |
| .map((n) => { |
| const path = pathFn(n.category_id); |
| return ` |
| <article class="note"> |
| <h2>${escapeHtml(n.title)}${n.pinned ? ' <span class="pin">📌</span>' : ""}</h2> |
| <div class="meta">${path ? "📁 " + escapeHtml(path) + " · " : ""}Güncelleme: ${escapeHtml(fmt(n.updated_at))}</div> |
| <div class="content">${marked.parse(n.content || "_(boş)_")}</div> |
| </article>`; |
| }) |
| .join("\n"); |
|
|
| return `<!DOCTYPE html><html lang="tr"><head><meta charset="utf-8"> |
| <style> |
| * { box-sizing: border-box; } |
| body { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; color: #1a1d27; margin: 0; padding: 32px 40px; font-size: 13px; line-height: 1.6; } |
| h1.doc { font-size: 24px; margin: 0 0 4px; } |
| .doc-sub { color: #6b7280; font-size: 12px; margin-bottom: 24px; } |
| article.note { padding: 18px 22px; border: 1px solid #e5e7eb; border-radius: 12px; margin-bottom: 16px; break-inside: avoid; } |
| article.note h2 { font-size: 17px; margin: 0 0 4px; color: #111827; } |
| .meta { color: #6b7280; font-size: 11px; margin-bottom: 12px; } |
| .content { color: #1f2433; } |
| .content h1,.content h2,.content h3 { font-size: 14px; margin: 12px 0 6px; } |
| .content ul,.content ol { padding-left: 20px; margin: 6px 0; } |
| .content code { background: #f3f4f6; padding: 1px 5px; border-radius: 4px; font-size: 12px; } |
| .content pre { background: #f3f4f6; padding: 12px; border-radius: 8px; overflow-x: auto; } |
| .content blockquote { border-left: 3px solid #c7c9d1; margin: 8px 0; padding-left: 12px; color: #4b5563; } |
| .rem { margin-top: 12px; border-top: 1px dashed #e5e7eb; padding-top: 8px; } |
| .rem h3 { font-size: 12px; margin: 0 0 4px; color: #6b7280; text-transform: uppercase; letter-spacing: .05em; } |
| .rem ul { margin: 0; padding-left: 18px; font-size: 12px; color: #4b5563; } |
| .pin { font-size: 13px; } |
| </style></head> |
| <body> |
| <h1 class="doc">${escapeHtml(title)}</h1> |
| <div class="doc-sub">${notes.length} not · ${new Date().toLocaleDateString("tr-TR", { day: "2-digit", month: "long", year: "numeric" })}</div> |
| ${body || "<p>Not bulunamadı.</p>"} |
| </body></html>`; |
| } |
|
|
| |
| 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; |
| } |
|
|
| |
| 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); |
| local.writeUInt16LE(0, 8); |
| local.writeUInt32LE(0, 10); |
| 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); |
| cd.writeUInt16LE(0, 34); |
| cd.writeUInt16LE(0, 36); |
| cd.writeUInt32LE(0, 38); |
| 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]); |
| } |
|
|
| |
| 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(); |
| } |
| } |
|
|