~mplscorwin/orgvm

web proxy for org
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

refs

master
browse  log 

clone

read-only
https://git.sr.ht/~mplscorwin/orgvm
read/write
git@git.sr.ht:~mplscorwin/orgvm

You can also use your local clone with git send-email.

#Table of Contents

  1. Requirements
  2. Quick Start
  3. Usage
    1. Access A Document
    2. Select a Response Format
  4. Source
  5. LICENSE
  6. SOURCE
  7. COPYRIGHT

Web proxy for org.

  • access org docs via web-browser
  • automatically export to Markdown, HTML, PDF, or
  • easily download (instead of viewing)

#Requirements

#Quick Start

  1. download index.js into a new folder (cloning the repo works)
  2. review/edit configuration at top of index.js,
    • adjust basepath to the root folder of your org documents
    • edit port, if desired (default: 3000)
  3. npm install (adding express.js)
  4. node index.js (starting the program)
  5. navigate to http://localhost:3000/somedoc to test.

Where somedoc (in step #5) is an existing org filename given relative to basepath.

#Usage

#Access A Document

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.

#Select a Response Format

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  
pdf  

#Source

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}`)
})

#LICENSE

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/>.

#SOURCE

We're on sourcehut!

https://git.sr.ht/~mplscorwin/orgvm

We welcome your ideas.

Copyright 2021 by Corwin Brust, and contributers.