| @@ -0,0 +1 @@ | |||
| /node_modules | |||
| @@ -0,0 +1,99 @@ | |||
| { | |||
| "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=" | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,83 @@ | |||
| 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); | |||
| @@ -0,0 +1,33 @@ | |||
| <!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> | |||
| @@ -0,0 +1,152 @@ | |||
| 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); | |||
| @@ -0,0 +1,39 @@ | |||
| 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; | |||
| } | |||