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