From a90b3c888e3d10906e05416392923ccc4051e39e Mon Sep 17 00:00:00 2001
From: Nicola Clark <nicola@slottedspoon.dev>
Date: Sat, 23 Nov 2024 23:49:24 -0600
Subject: [PATCH] we have a rendered PDF!

TODOS:

* actually apply styles to PDF
* better error handling on client side
---
 .vscode/settings.json             |  3 +++
 Dockerfile                        |  9 +++++++--
 package-lock.json                 | 18 ++++++++++++++++++
 package.json                      |  1 +
 src/lib/components/preview.svelte | 19 ++++++++++++-------
 src/routes/render/+server.ts      | 22 +++++++++++++++++++---
 6 files changed, 60 insertions(+), 12 deletions(-)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index c9789f0..e988b6e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,12 +4,15 @@
     "doctypes",
     "esbenp",
     "iconify",
+    "pandoc",
+    "pdoc",
     "rehype",
     "resumarkdown",
     "résumé",
     "spacebar",
     "tablist",
     "testid",
+    "texlive",
     "textbox"
   ],
   "editor.formatOnSave": true,
diff --git a/Dockerfile b/Dockerfile
index 27d549c..35924e4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,11 +1,16 @@
-FROM node:22.11.0-alpine AS builder
+FROM node:22.11.0-bookworm-slim AS builder
 WORKDIR /app
 COPY . .
 RUN npm install && npm run build
 
-FROM node:22.11.0-alpine AS runner
+FROM node:22.11.0-bookworm-slim AS runner
 LABEL maintainer="Nicola Clark <nicola@slottedspoon.dev>"
 EXPOSE 3000
+ENV LANG=C.UTF-8
+RUN apt-get update && \
+  apt-get install -y pandoc texlive && \
+  rm -rf /var/lib/apt/lists/*
+RUN useradd -u 9999 pdoc
 WORKDIR /app
 COPY package.json package-lock.json ./
 COPY --from=builder /app/build ./build
diff --git a/package-lock.json b/package-lock.json
index cb7b280..7acd8d6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
         "@sveltejs/adapter-node": "^5.2.9",
         "@sveltejs/kit": "^2.5.27",
         "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0",
+        "@types/node": "^22.9.0",
         "@types/ua-parser-js": "^0.7.39",
         "@types/user-agents": "^1.0.4",
         "less": "^4.2.0",
@@ -1017,6 +1018,16 @@
       "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==",
       "license": "MIT"
     },
+    "node_modules/@types/node": {
+      "version": "22.9.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
+      "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.19.8"
+      }
+    },
     "node_modules/@types/resolve": {
       "version": "1.20.2",
       "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz",
@@ -3194,6 +3205,13 @@
         "node": "*"
       }
     },
+    "node_modules/undici-types": {
+      "version": "6.19.8",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
+      "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/unified": {
       "version": "11.0.5",
       "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
diff --git a/package.json b/package.json
index 301a480..4c69e36 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
     "@sveltejs/adapter-node": "^5.2.9",
     "@sveltejs/kit": "^2.5.27",
     "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0",
+    "@types/node": "^22.9.0",
     "@types/ua-parser-js": "^0.7.39",
     "@types/user-agents": "^1.0.4",
     "less": "^4.2.0",
diff --git a/src/lib/components/preview.svelte b/src/lib/components/preview.svelte
index 65a2381..e92a5ec 100644
--- a/src/lib/components/preview.svelte
+++ b/src/lib/components/preview.svelte
@@ -18,13 +18,18 @@
 
   async function performRender(event: MouseEvent) {
     event.preventDefault();
-    console.log(
-      await fetch('/render', {
-        method: 'POST',
-        body: await output,
-        headers: { 'content-type': 'text/html' },
-      }),
-    );
+    const renderResponse = await fetch('/render', {
+      method: 'POST',
+      body: await output,
+      headers: { 'content-type': 'text/html' },
+    });
+
+    if (!renderResponse.ok) {
+      return;
+    }
+
+    const docURL = URL.createObjectURL(await renderResponse.blob());
+    window.open(docURL, '_blank');
   }
 </script>
 
diff --git a/src/routes/render/+server.ts b/src/routes/render/+server.ts
index 5cd002e..a1f2699 100644
--- a/src/routes/render/+server.ts
+++ b/src/routes/render/+server.ts
@@ -1,4 +1,6 @@
-import { error, json, type RequestEvent } from '@sveltejs/kit';
+import child_process from 'node:child_process';
+
+import { error, type RequestEvent } from '@sveltejs/kit';
 
 export async function POST({ request }: RequestEvent) {
   if (!(request.headers.get('Content-Type') ?? 'bad').includes('text/html')) {
@@ -10,7 +12,21 @@ export async function POST({ request }: RequestEvent) {
   }
 
   const input = await request.text();
-  console.log('received input: ', input);
 
-  return json({ msg: 'to be implemented' });
+  const {
+    status: exitCode,
+    stderr: err,
+    stdout: output,
+  } = child_process.spawnSync('pandoc', ['--sandbox', '-f', 'html', '-t', 'pdf'], {
+    input,
+    timeout: 5000,
+    uid: 9999,
+  });
+
+  if (exitCode !== 0) {
+    console.error('pandoc errored: ', err.toString());
+    error(500, 'pandoc returned error response in server');
+  }
+
+  return new Response(output, { headers: { 'content-type': 'application/pdf' } });
 }
