when request is for an existing file just send it
recreate HTML,MD,PDF files before serving when org src changed
fixing various breakage from refactoring elisp gen into OrgDocument
Web proxy for org.
index.js into a new folder (cloning the repo works)basepath to the root folder of your org documentsport, if desired (default: 3000)npm install (adding express.js)node index.js (starting the program)Where somedoc (in step #5) is an existing org filename given relative to basepath.
To access a given document add the name after the base URI for your
server (e.g. http://localhost:300/some-document). Do not include
the .org file extension. You can give a path relative to basename.
WARNING: document names are not scrubbed, for example to prevent path traversal attacks. Please be careful.
The default response format is "inline" HTML, meaning shown directly in the web-browser (vs. provided as an "attachment" for download). The next section explains how to controls this.
The default response is HTML. The table following lists possible
response formats. To select a response format expressly add a
query string parameter called type and provide one of the values
from the first column of the table.
| type | notes |
| html | it's nice to have htmlize around |
| md | |
This section provides the source code used to "build" (in fact:
tangle) the program (index.js). If you aren't an emacs user and don't
have a copy of index.js, you can create one by pasting the code below
into a new file ".js" file. (Make sure to get all the code from this
section; the program may be split into several blocks, some surrounded
with (non-code, syntax error causing) commentary when you find this.)
// orgvm - org virtualization
const defaults = {
"about": {
"copyright": "Copyright 2021-2022 Corwin Brust <corwin@bru.st> and contributers",
"license": "AGPlv3+",
"src": "https://git.sr.ht/~mplscorwin/orgvm"
},
"debug": { "elisp": true },
"EMACS": "c:/emacs/bin/emacs",
"basepath": "d:/projects",
"loadPath": [],
"loadLibrary": [],
"type": [{
"name": "Org",
"ext": "org",
"mime": ["text/plain"],
},{
"name": "Hyper-text Markup Language",
"ext": "html",
"mime": ["text/html"],
"replace": true
},{
"name": "Markdown",
"ext": "md",
"mime": ["text/html"],
"replace": true,
"loadLibrary": ["ox-md"]
},{
"name": "Portable Document Format",
"ext": "pdf",
"mime": ["application/pdf"],
"replace": true,
"exportFun": () => "(org-latex-export-to-pdf)"
}],
"language":[ "perl" ]
}
const fs = require('fs')
const { spawn } = require('child_process');
const express = require('express')
const app = express()
const port = 3000
const path = './file.txt'
const filename =
(file,type) =>
`${defaults.basepath}/${file}.${type.ext}`;
class OrgDocument {
static DEBUG(flag=null,cb=(x=flag)=>x&&console.log(x)) {
if(flag) {
if(defaults.debug && defaults.debug[flag]) {
return cb()
}
} else if(defaults.debug) {
return cb()
}
return null;
}
static get EMACS() { return defaults.EMACS }
static Emacs(elisp, callback) {
OrgDocument.DEBUG('elisp', () => console.log(elisp));
spawn( OrgDocument.EMACS, ['-batch', '-eval', elisp])
.on('close', callback);
}
static type(type) {
return type.hasOwnProperty('ext')
? type
: defaults.type.find(x => x.ext == type)
}
static file(file,type) {
const ext = OrgDocument.type(type).ext;
const name = file.hasOwnProperty("name")
? file.name
: file;
return `${defaults.basepath}/${name}.${ext}`
}
constructor(options = {}) {
this.name = options.name;
this.type = options.type
? (options.type.hasOwnProperty('ext')
? options.type
: defaults.type.find( x => x.ext == options.type))
: defaults.type[0];
this.language = options.language || [];
this.load = { path: (options.loadPath
? (Array.isArray(options.loadPath)
? options.loadPath
: [options.loadPath])
: []),
libs: (options.loadLibrary
? (Array.isArray(options.loadLibrary)
? options.loadLibrary
: [options.loadLibrary])
: []) };
}
out(type=this.type) { return OrgDocument.file(this.name, type) }
get src() { return this.out("org") }
get path() { return this.out(this.type) }
formatLisp(args) {
return !Array.isArray(args)
? (typeof args === 'function'
? this.formatLisp(args.apply(this))
: args)
: '(' + args.map( x => this.formatLisp(x)).join(' ') + ')';
}
orgExport(type=this.type) {
const t = OrgDocument.type(type);
var prognOrLet = t.loadPath && t.loadPath.length
? ["let", [["load-path",
["append", "load-path",
["list"].concat(
t.loadPath.map( x => '"'+x+'"'))]]]]
: ["progn"];
return this.formatLisp(prognOrLet.concat(
[["find-file", '"' + this.path + '"']]
).concat(
t.loadLibrary && t.loadLibrary.length
? t.loadLibrary.map( x => [ "load-library", '"'+x+'"'])
: []
).concat(
[type.exportFun
? type.exportFun.bind(this).apply(t)
: [ "org-export-to-file",
"'"+t.ext,
' "'+this.out(t)+'"']
]));
}
sourceBlockExecute(name,language,callback) {
if (!this.language.includes(language)) {
return false;
}
const babelFile = defaults.basepath + '/' + this.name
+ '_babel_' + Date.now() + '-' + language + ".txt";
const elisp = this.formatLisp(
["progn",
["setq", "org-confirm-babel-evaluate", "nil"],
["org-babel-do-load-languages", "(quote org-babel-load-languages)",
"(quote (("+language+" . t)))"], // ZZZ handle a list?
["find-file", '"' + this.src + '"'],
["let", [["rv", ["org-sbe", '"' + name + '"']]],
["with-temp-buffer",
["insert", "rv"],
["write-file", '"'+babelFile+'"']]]]);
OrgDocument.Emacs(elisp, (ok) => callback(ok===0));
}
}
function fileExists(path) {
var rv = false;
try { if(fs.existsSync(path)) rv = true }
catch(err) { }
return rv;
}
function isNewer(srcFile, genFile) {
var isNewer = false;
try {
if(fs.statSync(srcFile).mtime > fs.statSync(genFile).mtime) {
isNewer = true;
}
} catch(e) { /*failure is ignored*/ }
return isNewer;
}
function useCached(doc, type) {
const tmpPath = doc.out(type);
if(fileExists(tmpPath)) {
if(type.replace === true) {
// file exists but we might need to replace it
// ok to use existing file unless doc is newer
return false === isNewer(doc.path, tmpPath);
}
return true; // file exists and we can't replace it
}
return false; // no prior file to use
}
app.get('/', (req, res) => res.sendFile("README.html"))
app.get('/:filename/run/:block', (req, res) => {
const doc = new OrgDocument({name:req.params.filename});
const block = req.params.block;
const babelFile = OrgDocument.file({name:'babel'},{ext:'txt'});
if(fileExists(doc.src)) {
const elisp = doc.formatLisp(
["progn",
["setq", "org-confirm-babel-evaluate", "nil"],
["org-babel-do-load-languages", "(quote org-babel-load-languages)",
"(quote ((perl . t)))"], // , ['perl', '.', 't'], ")"
["find-file", '"' + doc.src + '"'],
["let", [["rv", ["org-sbe", `"${block}"`]]],
["with-temp-buffer",
["insert", "rv"],
["write-file", '"'+babelFile+'"']]]]);
OrgDocument.Emacs(elisp, (ok) => {
if(ok === 0) {
res.sendFile(babelFile)
} else { // export fail
res.sendStatus(500);
}})
} else { // no such file
req.sendStatus(404);
}
});
app.get('/:filename', (req, res) => {
const filename = req.params.filename;
// special case: just send actual files
const realFile = `${defaults.basepath}/${filename}`;
if(fileExists(realFile)) {
res.sendFile(realFile);
return;
}
const doc = new OrgDocument({name:filename});
const type = OrgDocument.type( req.query.type || "html");
const file = doc.out(type);
const send = () => req.query.download ? res.download(file) : res.sendFile(file);
if(type) {
if(fileExists(doc.src)) {
var responseFile = doc.out(type);
if(true === useCached(doc, type)) {
send()
} else { // new format for this file
const elisp = doc.orgExport(type);
OrgDocument.Emacs(elisp, (ok) => {
if(ok === 0) {
send();
} else { // export fail
res.sendStatus(500);
}})}
} else { // unkown document
res.sendStatus(404)
}} else { // unknown type requested
res.sendStatus(401)
}
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
})
This section provides the license for this program. It is also used
to create (again, "tangle") LICENSE.txt when building the project.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU Affero Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
We're on sourcehut!
https://git.sr.ht/~mplscorwin/orgvm
We welcome your ideas.
Copyright 2021 by Corwin Brust, and contributers.