From 0811f1c3969ef6ea1090a02acfb1efc028071f6d Mon Sep 17 00:00:00 2001
From: Nicola Clark <nicola@slottedspoon.dev>
Date: Sun, 24 Nov 2024 00:53:07 -0600
Subject: [PATCH] interesting: turns out we don't actually need pandoc

---
 Dockerfile                        |   5 -
 package-lock.json                 | 328 +++++++++++++++++++++++++++++-
 package.json                      |   2 +
 src/lib/components/preview.svelte |  17 +-
 src/lib/render-preview.ts         |  26 ++-
 src/routes/render/+server.ts      |  32 ---
 6 files changed, 356 insertions(+), 54 deletions(-)
 delete mode 100644 src/routes/render/+server.ts

diff --git a/Dockerfile b/Dockerfile
index 35924e4..628e922 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -6,11 +6,6 @@ RUN npm install && npm run build
 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 7acd8d6..b1cf76d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,8 @@
       "name": "resumarkdown",
       "version": "0.0.1",
       "dependencies": {
+        "hast": "^0.0.2",
+        "hastscript": "^9.0.0",
         "iconify-icon": "^2.1.0",
         "rehype-document": "^7.0.3",
         "rehype-sanitize": "^6.0.0",
@@ -1218,6 +1220,15 @@
         "node": ">=12"
       }
     },
+    "node_modules/attach-ware": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/attach-ware/-/attach-ware-1.1.1.tgz",
+      "integrity": "sha512-OpavlXWZkyE7m28fpCWF/RmxCukC1edukJp9IKjEpZs/O11H3896DkLpK7lMiL8ZDx2yxo9FrZQaeHkyJGcIuQ==",
+      "license": "MIT",
+      "dependencies": {
+        "unherit": "^1.0.0"
+      }
+    },
     "node_modules/axobject-query": {
       "version": "4.1.0",
       "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -1248,6 +1259,15 @@
         "node": ">=8"
       }
     },
+    "node_modules/camelcase": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+      "integrity": "sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/ccount": {
       "version": "2.0.1",
       "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@@ -1331,6 +1351,12 @@
         "url": "https://paulmillr.com/funding/"
       }
     },
+    "node_modules/co": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/co/-/co-3.1.0.tgz",
+      "integrity": "sha512-CQsjCRiNObI8AtTsNIBDRMQ4oMR83CzEswHYahClvul7gKk+lDQiOKv+5qh7LQWf5sh6jkZNispz/QlsZxyNgA==",
+      "license": "MIT"
+    },
     "node_modules/comma-separated-tokens": {
       "version": "2.0.3",
       "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@@ -1450,6 +1476,80 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/dom-serializer": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
+      "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==",
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^2.0.1",
+        "entities": "^2.0.0"
+      }
+    },
+    "node_modules/dom-serializer/node_modules/domelementtype": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
+      "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fb55"
+        }
+      ],
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/dom-serializer/node_modules/entities": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
+      "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==",
+      "license": "BSD-2-Clause",
+      "funding": {
+        "url": "https://github.com/fb55/entities?sponsor=1"
+      }
+    },
+    "node_modules/domelementtype": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz",
+      "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/domhandler": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz",
+      "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "domelementtype": "1"
+      }
+    },
+    "node_modules/domutils": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz",
+      "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==",
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "dom-serializer": "0",
+        "domelementtype": "1"
+      }
+    },
+    "node_modules/ent": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.1.tgz",
+      "integrity": "sha512-QHuXVeZx9d+tIQAz/XztU0ZwZf2Agg9CcXcgE1rurqvdBeDBrpSwjl8/6XUqMg7tw2Y7uAdKb2sRv+bSEFqQ5A==",
+      "license": "MIT",
+      "dependencies": {
+        "punycode": "^1.4.1"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/entities": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz",
+      "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==",
+      "license": "BSD-2-Clause"
+    },
     "node_modules/errno": {
       "version": "0.1.8",
       "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz",
@@ -1503,6 +1603,12 @@
         "@esbuild/win32-x64": "0.21.5"
       }
     },
+    "node_modules/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
     "node_modules/esm-env": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
@@ -1612,6 +1718,23 @@
         "node": ">= 0.4"
       }
     },
+    "node_modules/hast": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/hast/-/hast-0.0.2.tgz",
+      "integrity": "sha512-1MrzC9MtAYhzLix2w++pGEtRyCm6N1fcxCjx+1xJo/92fNDRFemFaum18XWd8No3f+FgT9lv6fKOC8LZRcxxuw==",
+      "license": "MIT",
+      "dependencies": {
+        "bail": "^1.0.0",
+        "camelcase": "^1.2.1",
+        "ent": "^2.2.0",
+        "escape-html": "^1.0.3",
+        "htmlparser2": "^3.8.3",
+        "param-case": "^1.1.1",
+        "property-information": "^2.0.0",
+        "trim": "0.0.1",
+        "unified": "^2.1.0"
+      }
+    },
     "node_modules/hast-util-parse-selector": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz",
@@ -1676,10 +1799,46 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/hast/node_modules/bail": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz",
+      "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==",
+      "license": "MIT",
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
+    "node_modules/hast/node_modules/property-information": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/property-information/-/property-information-2.0.0.tgz",
+      "integrity": "sha512-8oVcjnCeqANq/exCzgse3D47GBmgOuI47vNya7xBIJhUXeh49AjZuXWw2gTh1UuN4rfwz5pEv2ZFzu45vBby5A==",
+      "license": "MIT"
+    },
+    "node_modules/hast/node_modules/unified": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/unified/-/unified-2.1.4.tgz",
+      "integrity": "sha512-qa4nA26ms49OczPueTt7G46r89TOlwAJ4pEk2U4mwkV1wNhjttItF03SE/YnfkgWg14tzmAHXGhJp2GhDYwn1A==",
+      "license": "MIT",
+      "dependencies": {
+        "attach-ware": "^1.0.0",
+        "bail": "^1.0.0",
+        "extend": "^3.0.0",
+        "unherit": "^1.0.4",
+        "vfile": "^1.0.0",
+        "ware": "^1.3.0"
+      }
+    },
+    "node_modules/hast/node_modules/vfile": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-1.4.0.tgz",
+      "integrity": "sha512-7Fz639rwERslMqQCuf1/0H4Tqe2q484Xl6X/jsKqrP7IjFcDODFURhv0GekMnImpbj9pTOojtqL7r39LJJkjGA==",
+      "license": "MIT"
+    },
     "node_modules/hastscript": {
-      "version": "8.0.0",
-      "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
-      "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
+      "version": "9.0.0",
+      "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.0.tgz",
+      "integrity": "sha512-jzaLBGavEDKHrc5EfFImKN7nZKKBdSLIdGvCwDZ9TfzbF2ffXiov8CKE445L2Z1Ek2t/m4SKQ2j6Ipv7NyUolw==",
       "license": "MIT",
       "dependencies": {
         "@types/hast": "^3.0.0",
@@ -1703,6 +1862,20 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/htmlparser2": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz",
+      "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "domelementtype": "^1.3.1",
+        "domhandler": "^2.3.0",
+        "domutils": "^1.5.1",
+        "entities": "^1.1.1",
+        "inherits": "^2.0.1",
+        "readable-stream": "^3.1.1"
+      }
+    },
     "node_modules/iconify-icon": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/iconify-icon/-/iconify-icon-2.1.0.tgz",
@@ -1754,6 +1927,12 @@
         "url": "https://github.com/sponsors/wooorm"
       }
     },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
     "node_modules/is-core-module": {
       "version": "2.15.1",
       "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
@@ -1876,6 +2055,12 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/lower-case": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz",
+      "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==",
+      "license": "MIT"
+    },
     "node_modules/magic-string": {
       "version": "0.30.11",
       "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
@@ -2478,6 +2663,15 @@
         "node": ">= 4.4.x"
       }
     },
+    "node_modules/param-case": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/param-case/-/param-case-1.1.2.tgz",
+      "integrity": "sha512-gksk6zeZQxwBm1AHsKh+XDFsTGf1LvdZSkkpSIkfDtzW+EQj/P2PBgNb3Cs0Y9Xxqmbciv2JZe3fWU6Xbher+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "sentence-case": "^1.1.2"
+      }
+    },
     "node_modules/parse-node-version": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz",
@@ -2681,6 +2875,26 @@
       "license": "MIT",
       "optional": true
     },
+    "node_modules/punycode": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+      "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+      "license": "MIT"
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
     "node_modules/readdirp": {
       "version": "4.0.2",
       "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
@@ -2711,6 +2925,23 @@
         "url": "https://opencollective.com/unified"
       }
     },
+    "node_modules/rehype-document/node_modules/hastscript": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz",
+      "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/hast": "^3.0.0",
+        "comma-separated-tokens": "^2.0.0",
+        "hast-util-parse-selector": "^4.0.0",
+        "property-information": "^6.0.0",
+        "space-separated-tokens": "^2.0.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/unified"
+      }
+    },
     "node_modules/rehype-sanitize": {
       "version": "6.0.0",
       "resolved": "https://registry.npmjs.org/rehype-sanitize/-/rehype-sanitize-6.0.0.tgz",
@@ -2840,6 +3071,26 @@
         "node": ">=6"
       }
     },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
     "node_modules/safer-buffer": {
       "version": "2.1.2",
       "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
@@ -2867,6 +3118,15 @@
         "semver": "bin/semver"
       }
     },
+    "node_modules/sentence-case": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-1.1.3.tgz",
+      "integrity": "sha512-laa/UDTPXsrQnoN/Kc8ZO7gTeEjMsuPiDgUCk9N0iINRZvqAMCTXjGl8+tD27op1eF/JHbdUlEUmovDh6AX7sA==",
+      "license": "MIT",
+      "dependencies": {
+        "lower-case": "^1.1.1"
+      }
+    },
     "node_modules/set-cookie-parser": {
       "version": "2.7.0",
       "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.0.tgz",
@@ -2941,6 +3201,15 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
     "node_modules/stringify-entities": {
       "version": "4.0.4",
       "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
@@ -3138,6 +3407,12 @@
         "node": ">=6"
       }
     },
+    "node_modules/trim": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz",
+      "integrity": "sha512-YzQV+TZg4AxpKxaTHK3c3D+kRDCGVEE7LemdlQZoQXn0iennk10RsIoY6ikzAqJTc9Xjl9C1/waHom/J86ziAQ==",
+      "deprecated": "Use String.prototype.trim() instead"
+    },
     "node_modules/trim-lines": {
       "version": "3.0.1",
       "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -3212,6 +3487,20 @@
       "dev": true,
       "license": "MIT"
     },
+    "node_modules/unherit": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz",
+      "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.0",
+        "xtend": "^4.0.0"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/wooorm"
+      }
+    },
     "node_modules/unified": {
       "version": "11.0.5",
       "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
@@ -3309,6 +3598,12 @@
         "lodash.clonedeep": "^4.5.0"
       }
     },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
     "node_modules/vfile": {
       "version": "6.0.3",
       "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -3518,6 +3813,15 @@
         }
       }
     },
+    "node_modules/ware": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/ware/-/ware-1.3.0.tgz",
+      "integrity": "sha512-Y2HUDMktriUm+SR2gZWxlrszcgtXExlhQYZ8QJNYbl22jum00KIUcHJ/h/sdAXhWTJcbSkiMYN9Z2tWbWYSrrw==",
+      "license": "MIT",
+      "dependencies": {
+        "wrap-fn": "^0.1.0"
+      }
+    },
     "node_modules/why-is-node-running": {
       "version": "2.3.0",
       "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
@@ -3535,6 +3839,24 @@
         "node": ">=8"
       }
     },
+    "node_modules/wrap-fn": {
+      "version": "0.1.5",
+      "resolved": "https://registry.npmjs.org/wrap-fn/-/wrap-fn-0.1.5.tgz",
+      "integrity": "sha512-xDLdGx0M8JQw9QDAC9s5NUxtg9MI09F6Vbxa2LYoSoCvzJnx2n81YMIfykmXEGsUvuLaxnblJTzhSOjUOX37ag==",
+      "license": "MIT",
+      "dependencies": {
+        "co": "3.1.0"
+      }
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    },
     "node_modules/yaml": {
       "version": "1.10.2",
       "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
diff --git a/package.json b/package.json
index 4c69e36..902d2cf 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,8 @@
     "format": "prettier --write ."
   },
   "dependencies": {
+    "hast": "^0.0.2",
+    "hastscript": "^9.0.0",
     "iconify-icon": "^2.1.0",
     "rehype-document": "^7.0.3",
     "rehype-sanitize": "^6.0.0",
diff --git a/src/lib/components/preview.svelte b/src/lib/components/preview.svelte
index e92a5ec..8181faf 100644
--- a/src/lib/components/preview.svelte
+++ b/src/lib/components/preview.svelte
@@ -16,20 +16,11 @@
 
   let output: Promise<string> = $derived(renderPreview(markdown, stylesheet));
 
+  let previewFrame = $state<HTMLIFrameElement | undefined>();
+
   async function performRender(event: MouseEvent) {
     event.preventDefault();
-    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');
+    previewFrame?.contentWindow?.postMessage('print');
   }
 </script>
 
@@ -37,7 +28,7 @@
   {#await output}
     <p>processing...</p>
   {:then result}
-    <iframe title="résumé preview" srcdoc={result}></iframe>
+    <iframe title="résumé preview" srcdoc={result} bind:this={previewFrame}></iframe>
     <button type="submit" onclick={performRender}>
       <span>download</span>
       <iconify-icon icon="ion:download-outline" height="1.25em"></iconify-icon>
diff --git a/src/lib/render-preview.ts b/src/lib/render-preview.ts
index 62ca291..8932383 100644
--- a/src/lib/render-preview.ts
+++ b/src/lib/render-preview.ts
@@ -1,9 +1,32 @@
+import { type Element as HastElement, type Root as HastRoot } from 'hast';
+import { h } from 'hastscript';
 import rehypeDocument from 'rehype-document';
 import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
 import rehypeStringify from 'rehype-stringify';
 import remarkParse from 'remark-parse';
 import remarkRehype from 'remark-rehype';
-import { unified } from 'unified';
+import { unified, type Plugin } from 'unified';
+
+const printScript = `
+window.addEventListener('message', (evt) => {
+  if (evt.data === 'print') {
+    window.print();
+  }
+});
+`;
+
+const injectPrintScript: Plugin<[], HastRoot> = function () {
+  return (tree) => {
+    const printScriptNode = h('script', printScript);
+    const htmlNode = tree.children.find(
+      (node) => node.type === 'element' && node.tagName === 'html',
+    ) as HastElement;
+    const bodyNode = htmlNode.children.find(
+      (node) => node.type === 'element' && node.tagName === 'body',
+    ) as HastElement;
+    bodyNode.children.push(printScriptNode);
+  };
+};
 
 const renderPreview = async (markdown: string, stylesheet: string): Promise<string> => {
   let allowedTags: string[] = ['body', 'head', 'html', 'style'];
@@ -20,6 +43,7 @@ const renderPreview = async (markdown: string, stylesheet: string): Promise<stri
         allowDoctypes: true,
         tagNames: allowedTags,
       })
+      .use(injectPrintScript)
       .use(rehypeStringify)
       .process(markdown),
   );
diff --git a/src/routes/render/+server.ts b/src/routes/render/+server.ts
deleted file mode 100644
index a1f2699..0000000
--- a/src/routes/render/+server.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-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')) {
-    error(400, 'request body not HTML');
-  }
-
-  if (!request.body) {
-    error(400, 'no body in request');
-  }
-
-  const input = await request.text();
-
-  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' } });
-}
