| /node_modules |
| { | |||||
| "requires": true, | |||||
| "lockfileVersion": 1, | |||||
| "dependencies": { | |||||
| "balanced-match": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", | |||||
| "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" | |||||
| }, | |||||
| "brace-expansion": { | |||||
| "version": "1.1.11", | |||||
| "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", | |||||
| "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", | |||||
| "requires": { | |||||
| "balanced-match": "^1.0.0", | |||||
| "concat-map": "0.0.1" | |||||
| } | |||||
| }, | |||||
| "concat-map": { | |||||
| "version": "0.0.1", | |||||
| "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", | |||||
| "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" | |||||
| }, | |||||
| "fs.realpath": { | |||||
| "version": "1.0.0", | |||||
| "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", | |||||
| "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" | |||||
| }, | |||||
| "glob": { | |||||
| "version": "7.1.4", | |||||
| "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", | |||||
| "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", | |||||
| "requires": { | |||||
| "fs.realpath": "^1.0.0", | |||||
| "inflight": "^1.0.4", | |||||
| "inherits": "2", | |||||
| "minimatch": "^3.0.4", | |||||
| "once": "^1.3.0", | |||||
| "path-is-absolute": "^1.0.0" | |||||
| } | |||||
| }, | |||||
| "inflight": { | |||||
| "version": "1.0.6", | |||||
| "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", | |||||
| "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", | |||||
| "requires": { | |||||
| "once": "^1.3.0", | |||||
| "wrappy": "1" | |||||
| } | |||||
| }, | |||||
| "inherits": { | |||||
| "version": "2.0.4", | |||||
| "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", | |||||
| "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" | |||||
| }, | |||||
| "minimatch": { | |||||
| "version": "3.0.4", | |||||
| "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", | |||||
| "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", | |||||
| "requires": { | |||||
| "brace-expansion": "^1.1.7" | |||||
| } | |||||
| }, | |||||
| "once": { | |||||
| "version": "1.4.0", | |||||
| "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", | |||||
| "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", | |||||
| "requires": { | |||||
| "wrappy": "1" | |||||
| } | |||||
| }, | |||||
| "path-is-absolute": { | |||||
| "version": "1.0.1", | |||||
| "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", | |||||
| "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" | |||||
| }, | |||||
| "rimraf": { | |||||
| "version": "2.7.1", | |||||
| "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", | |||||
| "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", | |||||
| "requires": { | |||||
| "glob": "^7.1.3" | |||||
| } | |||||
| }, | |||||
| "tmp": { | |||||
| "version": "0.1.0", | |||||
| "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.1.0.tgz", | |||||
| "integrity": "sha512-J7Z2K08jbGcdA1kkQpJSqLF6T0tdQqpR2pnSUXsIchbPdTI9v3e85cLW0d6WDhwuAleOV71j2xWs8qMPfK7nKw==", | |||||
| "requires": { | |||||
| "rimraf": "^2.6.3" | |||||
| } | |||||
| }, | |||||
| "wrappy": { | |||||
| "version": "1.0.2", | |||||
| "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", | |||||
| "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" | |||||
| } | |||||
| } | |||||
| } |
| let fs = require("fs"); | |||||
| let http = require("http"); | |||||
| let pathlib = require("path"); | |||||
| let spawn = require("child_process").spawn; | |||||
| let tmp = require("tmp"); | |||||
| function processPandoc(req, res) { | |||||
| let tmpf = tmp.fileSync({ postfix: ".pdf" }); | |||||
| let child = spawn("pandoc", | |||||
| [ "--standalone", "--pdf-engine=pdflatex", "--output", tmpf.name ]); | |||||
| child.on("error", err => { | |||||
| res.writeHead(500); | |||||
| res.end(err); | |||||
| tmpf.removeCallback(); | |||||
| }); | |||||
| let output = ""; | |||||
| child.stdout.on("data", d => output += d.toString()); | |||||
| child.stderr.on("data", d => output += d.toString()); | |||||
| child.on("exit", (code, sig) => { | |||||
| if (code === 0) { | |||||
| res.end(JSON.stringify({ | |||||
| pdf: fs.readFileSync(tmpf.name).toString("base64"), | |||||
| output | |||||
| })); | |||||
| tmpf.removeCallback(); | |||||
| } else { | |||||
| res.end(JSON.stringify({ output })); | |||||
| tmpf.removeCallback(); | |||||
| } | |||||
| }); | |||||
| req.pipe(child.stdin); | |||||
| } | |||||
| function mime(path) { | |||||
| if (path.endsWith(".html")) | |||||
| return "text/html"; | |||||
| if (path.endsWith(".css")) | |||||
| return "text/css"; | |||||
| if (path.endsWith(".js")) | |||||
| return "application/javascript"; | |||||
| return "application/octet-stream"; | |||||
| } | |||||
| function serve(url, res) { | |||||
| let path = pathlib.join(__dirname, "web", url); | |||||
| let rs = fs.createReadStream(path); | |||||
| rs.once("error", err => { | |||||
| if (err.code == "ENOENT") { | |||||
| res.writeHead(404); | |||||
| res.end("404 Not Found"); | |||||
| } else { | |||||
| res.writeHead(500); | |||||
| res.end("500 Internal Server Error"); | |||||
| console.error(err); | |||||
| } | |||||
| }); | |||||
| rs.once("open", () => { | |||||
| res.writeHead(200, { | |||||
| "Content-Type": mime(path) | |||||
| }); | |||||
| rs.pipe(res); | |||||
| }); | |||||
| } | |||||
| let server = http.createServer((req, res) => { | |||||
| if (req.url == "/") { | |||||
| serve("/index.html", res); | |||||
| } else if (req.method == "POST" && req.url == "/pandoc") { | |||||
| processPandoc(req, res); | |||||
| } else { | |||||
| serve(req.url, res); | |||||
| } | |||||
| }); | |||||
| let port = process.env.PORT || 8080; | |||||
| server.listen(port, "127.0.0.1"); | |||||
| console.log("Listening to 127.0.0.1:" + port); |
| <!DOCTYPE html> | |||||
| <html> | |||||
| <head> | |||||
| <meta charset="utf-8"> | |||||
| <title>Pandoc As A Service</title> | |||||
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.css"> | |||||
| <link rel="stylesheet" href="style.css"> | |||||
| </head> | |||||
| <body> | |||||
| <div id="container"> | |||||
| <div id="editor-container"> | |||||
| <textarea id="editor"></textarea> | |||||
| </div> | |||||
| <div id="preview-container"> | |||||
| <div id="preview-controls"> | |||||
| <button id="preview-prev-btn"><</button> | |||||
| <span id="preview-state">0/0</span> | |||||
| <button id="preview-next-btn">></button> | |||||
| </div> | |||||
| <div id="preview-wrapper"> | |||||
| <canvas id="preview"></canvas> | |||||
| </div> | |||||
| </div> | |||||
| <div id="pandoc-output"> | |||||
| Test | |||||
| </div> | |||||
| </div> | |||||
| <script src="https://cdn.jsdelivr.net/simplemde/latest/simplemde.min.js"></script> | |||||
| <script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@2.1.266/build/pdf.min.js"></script> | |||||
| <script src="/script.js"></script> | |||||
| </body> | |||||
| </html> |
| var pdfjsLib = window['pdfjs-dist/build/pdf']; | |||||
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@2.1.266/build/pdf.worker.min.js'; | |||||
| class PdfRenderer { | |||||
| constructor(canvas) { | |||||
| this.numPages = 1; | |||||
| this.currentPage = 0; | |||||
| this.scale = 2; | |||||
| this.canvas = canvas; | |||||
| this.ctx = this.canvas.getContext("2d"); | |||||
| this.offScreenCanvas = document.createElement("canvas"); | |||||
| this.offScreenCtx = this.offScreenCanvas.getContext("2d"); | |||||
| this.dpr = window.devicePixelRatio || 1; | |||||
| this.pdf = null; | |||||
| this.rendering = false; | |||||
| this.renderPending = false; | |||||
| } | |||||
| update(b64, cb) { | |||||
| if (b64 == "") { | |||||
| this.pdf = null; | |||||
| this.numPages = 1; | |||||
| this.currentPage = 0; | |||||
| this.render(); | |||||
| cb(); | |||||
| return; | |||||
| } else if (b64 == null) { | |||||
| cb(); | |||||
| return; | |||||
| } | |||||
| let data = atob(b64); | |||||
| let loadingTask = pdfjsLib.getDocument({ data }); | |||||
| loadingTask.promise.then(pdf => { | |||||
| this.pdf = pdf; | |||||
| this.numPages = this.pdf.numPages; | |||||
| if (this.currentPage >= this.numPages) | |||||
| this.currentPage = this.numPages - 1; | |||||
| this.render(); | |||||
| cb(); | |||||
| }); | |||||
| } | |||||
| render() { | |||||
| if (this.rendering) { | |||||
| this.renderPending = true; | |||||
| return; | |||||
| } | |||||
| if (this.pdf == null) { | |||||
| this.canvas.width = this.canvas.width; | |||||
| return; | |||||
| } | |||||
| this.rendering = true; | |||||
| this.pdf.getPage(this.currentPage + 1).then(page => { | |||||
| let viewport = page.getViewport({ scale: this.scale }); | |||||
| this.offScreenCanvas.height = viewport.height * this.dpr; | |||||
| this.offScreenCanvas.width = viewport.width * this.dpr; | |||||
| this.offScreenCanvas.style.width = viewport.width + "px"; | |||||
| this.offScreenCanvas.style.height = viewport.height + "px"; | |||||
| this.offScreenCtx.scale(this.dpr, this.dpr); | |||||
| let renderTask = page.render({ canvasContext: this.offScreenCtx, viewport }); | |||||
| renderTask.promise.then(() => { | |||||
| this.canvas.width = this.offScreenCanvas.width; | |||||
| this.canvas.height = this.offScreenCanvas.height; | |||||
| this.canvas.style.width = this.offScreenCanvas.style.width; | |||||
| this.canvas.style.height = this.offScreenCanvas.style.height; | |||||
| this.ctx.drawImage(this.offScreenCanvas, 0, 0); | |||||
| this.rendering = false; | |||||
| if (this.renderPending) { | |||||
| this.renderPending = false; | |||||
| this.render(); | |||||
| } | |||||
| }); | |||||
| }); | |||||
| } | |||||
| } | |||||
| let renderer = new PdfRenderer(document.getElementById("preview")); | |||||
| let previewState = document.getElementById("preview-state"); | |||||
| let renderOutput = document.getElementById("pandoc-output"); | |||||
| function updatePreviewState() { | |||||
| previewState.innerText = (renderer.currentPage + 1) + "/" + renderer.numPages; | |||||
| } | |||||
| let markdownReqNum = 0; | |||||
| let markdownReqLast = 0; | |||||
| function renderMarkdown(canvas, text) { | |||||
| let num = markdownReqNum++; | |||||
| fetch("/pandoc", { | |||||
| method: "POST", | |||||
| body: editor.value(), | |||||
| }).then(res => res.json()).then(obj => { | |||||
| if (num < markdownReqLast) | |||||
| return; | |||||
| markdownReqLast = num; | |||||
| renderOutput.innerText = obj.output; | |||||
| renderer.update(obj.pdf, () => { | |||||
| updatePreviewState(); | |||||
| }); | |||||
| }); | |||||
| } | |||||
| function nextPage() { | |||||
| if (renderer.currentPage < renderer.numPages - 1) { | |||||
| renderer.currentPage += 1; | |||||
| renderer.render(); | |||||
| updatePreviewState(); | |||||
| } | |||||
| } | |||||
| function prevPage() { | |||||
| if (renderer.currentPage > 0) { | |||||
| renderer.currentPage -= 1; | |||||
| renderer.render(); | |||||
| updatePreviewState(); | |||||
| } | |||||
| } | |||||
| function debounce(ms, cb) { | |||||
| let timeout = null; | |||||
| return function() { | |||||
| if (timeout != null) { | |||||
| clearTimeout(timeout); | |||||
| timout = null; | |||||
| } | |||||
| timeout = setTimeout(cb, ms); | |||||
| } | |||||
| } | |||||
| let editor = new SimpleMDE({ | |||||
| element: document.getElementById("editor"), | |||||
| hideIcons: [ "side-by-side", "preview" ], | |||||
| spellChecker: false, | |||||
| autosave: true, | |||||
| }); | |||||
| let onChange = debounce(100, () => renderMarkdown(preview, editor.value())); | |||||
| editor.codemirror.on("change", onChange); | |||||
| renderMarkdown(preview, editor.value()); | |||||
| document.getElementById("preview-prev-btn").addEventListener("click", prevPage); | |||||
| document.getElementById("preview-next-btn").addEventListener("click", nextPage); |
| html, body, #container { | |||||
| height: 100%; | |||||
| margin: 0px; | |||||
| box-sizing: border-box; | |||||
| } | |||||
| #container { | |||||
| display: grid; | |||||
| grid-template-columns: 1fr 1fr; | |||||
| padding: 10px; | |||||
| grid-column-gap: 10px; | |||||
| grid-template-rows: auto 100px; | |||||
| grid-template-areas: | |||||
| 'left-pane right-pane' | |||||
| 'footer footer'; | |||||
| } | |||||
| #editor-container { | |||||
| grid-area: left-pane; | |||||
| min-height: 0px; | |||||
| display: grid; | |||||
| grid-template-rows: 50px auto 25px; | |||||
| overflow: auto; | |||||
| } | |||||
| #preview-container { | |||||
| grid-area: right-pane; | |||||
| min-height: 0px; | |||||
| display: grid; | |||||
| grid-template-rows: 50px auto; | |||||
| } | |||||
| #preview-wrapper { | |||||
| overflow: auto; | |||||
| } | |||||
| #pandoc-output { | |||||
| grid-area: footer; | |||||
| } |