/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; | |||||
} |