Browse Source

initial commit

master
Martin Dørum 4 years ago
commit
0c54e8e04c
6 changed files with 407 additions and 0 deletions
  1. 1
    0
      .gitignore
  2. 99
    0
      package-lock.json
  3. 83
    0
      server.js
  4. 33
    0
      web/index.html
  5. 152
    0
      web/script.js
  6. 39
    0
      web/style.css

+ 1
- 0
.gitignore View File

@@ -0,0 +1 @@
/node_modules

+ 99
- 0
package-lock.json View File

@@ -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="
}
}
}

+ 83
- 0
server.js View File

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

+ 33
- 0
web/index.html View File

@@ -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">&lt;</button>
<span id="preview-state">0/0</span>
<button id="preview-next-btn">&gt;</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>

+ 152
- 0
web/script.js View File

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

+ 39
- 0
web/style.css View File

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

Loading…
Cancel
Save