to-do / export.js
rodopsin
Bulut dağıtımı: libSQL/Turso + Docker (HF Spaces)
483e583
Raw
History Blame Contribute Delete
7.49 kB
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
// ---- 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 `
<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>`;
}
// ---- 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();
}
}