metatroncubeswdev c309581df0 Initial commit: Quorla social media manager
Dashboard (content library, publishing queue with approvals, inbox, team
management, AI runs), Meta connector, branded PDF reports, agent skills,
and Docker packaging (Dockerfile, docker-compose with Caddy HTTPS,
backup and path-relocation scripts).

Client data, live data and secrets are excluded via .gitignore.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:02:29 -04:00

48 lines
2.1 KiB
JavaScript

// Client documents: render markdown for the in-app viewer, export/serve branded PDFs.
import fs from "node:fs";
import path from "node:path";
import { marked } from "marked";
import { safeSlug } from "./clients.js";
import { toPdf } from "../../tools/report-pdf/build.mjs";
/** Resolve a document path inside one client's folder (blocks ../ escapes). */
export function resolveDoc(slug, rel) {
const dir = safeSlug(slug);
const full = path.resolve(dir, String(rel || ""));
if (!full.startsWith(dir + path.sep) || !/\.(md|pdf)$/.test(full) || !fs.existsSync(full)) {
throw Object.assign(new Error("Document not found."), { status: 404 });
}
return full;
}
function splitFrontMatter(md) {
const m = md.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
if (!m) return { meta: {}, body: md };
const meta = {};
const kpis = [];
for (const line of m[1].split(/\r?\n/)) {
const kpi = line.match(/^\s+-\s+(.*)$/);
if (kpi) { const [value, label = "", note = ""] = kpi[1].split("|").map((s) => s.trim()); kpis.push({ value, label, note }); continue; }
const kv = line.match(/^(\w+):\s*(.*)$/);
if (kv && kv[1] !== "kpis") meta[kv[1]] = kv[2].trim();
}
return { meta: { ...meta, kpis }, body: md.slice(m[0].length) };
}
export function renderDoc(slug, rel) {
const full = resolveDoc(slug, rel);
if (!full.endsWith(".md")) throw Object.assign(new Error("Only markdown documents can be previewed."), { status: 400 });
const { meta, body } = splitFrontMatter(fs.readFileSync(full, "utf8"));
// Content is written by our own team/AI runs; links open in a new tab.
const html = marked.parse(body, { gfm: true, breaks: true }).replace(/<a href=/g, '<a target="_blank" rel="noopener" href=');
const pdf = full.replace(/\.md$/, ".pdf");
return { path: rel, meta, html, updatedAt: fs.statSync(full).mtime.toISOString(), hasPdf: fs.existsSync(pdf) };
}
export function exportPdf(slug, rel) {
const full = resolveDoc(slug, rel);
if (!full.endsWith(".md")) throw Object.assign(new Error("Pick a markdown document."), { status: 400 });
const written = toPdf(full);
return path.relative(safeSlug(slug), written).replace(/\\/g, "/");
}