How to quickly turn an org-mode tree to JSX ?
org-mode is made of lisp and lisp can easily be turned
to json. The following does so, removing cycles, even
if we could keep them with objects{}
and references.
The other one below turns that JSON to a react JSX tree. So technically, it's not turning seamlessly org-mode to JSX and the JSON file has to be provided to the JS interperter of the browser.
Converting an elisp org structure to JSON brings a lot of insight about org itself.
org-to-json
;; Answer given by John Kitchin on stackx
(require 'json)
(defun org-export-json-buffer ()
(interactive)
(let* ((tree (org-element-parse-buffer 'object nil)))
(org-element-map tree (append org-element-all-elements
org-element-all-objects '(plain-text))
(lambda (x)
(if (org-element-property :parent x)
(org-element-put-property x :parent "none"))
(if (org-element-property :structure x)
(org-element-put-property x :structure "none")))
)
(with-current-buffer (get-buffer-create "*ox-json*")
(erase-buffer)
(insert (json-encode tree))
(json-pretty-print-buffer))
(switch-to-buffer (get-buffer-create "*ox-json*"))))
(defun org-to-json (query)
"Export headings that match QUERY to json."
(org-map-entries
(lambda ()
(org-copy-subtree)
(with-current-buffer (get-buffer-create "-t-")
(yank)))
query)
(with-current-buffer "-t-" (org-export-json-buffer))
(kill-buffer "-t-"))
(defun org-export-published-to-json ()
(org-map-entries
(lambda ()
(org-copy-subtree)
(with-current-buffer (get-buffer-create "-t-")
(yank)))
"TODO=\"PUBLISHED\"")
(with-current-buffer "-t-" (org-export-json-buffer))
(kill-buffer "-t-"))
json2vdom
/**
* orgβjsonβpreact
* author: ππππ‘ ππ¨π©ππ«ππ‘ @ ππ§ππγππ§ (c) 2016
* json2vdom.js
* date:<26/05/17>
*/
import {h} from 'preact';
import {CLS} from './orgnodes'
/* name, properties, children...(=> a vnode)
[ name, [ {prop1:a, props2:b,...} ], [ children...] ] β
<node prop1='a' prop2='b'> <children...> </node>
*/
export const rendervnode =(name, attributes, ...children)=> h(
CLS[name] || 'div',
{
...attributes,
...{class: name},
},
(children.length && children[0]) ? renderList(children) : null
)
export const renderList =(ar)=> ar && ar.map(e => {
if (typeof e === 'string') {
return e
}
if (Array.isArray(e)) {
return rendervnode(...e)
}
}
here is an alternative to write directly the vdom to the window.document https://jasonformat.com/wtf-is-jsx/
preact-org.json
/**
* preact-org-json
* @author Phil ESTIVAL
* Date:<05/04/17$ 20:45$>
* license : GPL
* org-json-nodes.js
*
* first letters of the orgjson's element are not capital
* that's why here some classes names aren't too.
* Those can't be instanciated from inside code (react convention)
* but can be associated in the export below
* or subclassed if needed
*
* todo : fix the [null] element in empty links
*/
import {Component} from 'preact'
import {renderList} from 'json2vdom.js'
class keyword extends Component {
render =()=>
<div class="keyword">
<div class={this.props.kw}>{this.props.kw}</div>
<div class={this.props.kw}>{this.props.value}</div>
</div>;
}
class Priority extends Component {
render =()=> <div class="priority">{ String.fromCharCode(this.props.priority) } </div>
}
class headline extends Component {
// a ctor for the headline will prevent upper nodes from rendering
toggle() {
this.setState({open:!this.state.open})
}
constructor() {
super(...arguments);
this.setState({open:true})
}
render({children},{},{}) { // yup, this.props.children
this.id = this.props.title && this.props.title.length>1 && this.props.title[1] || this.props.title; // ??? bof
//console.log('>>>>' + this.id, this.props.title)
this.title = renderList(this.props.title);
//this.tags = this.props.tags && this.props.tags.join(", ")
this.priority = this.props.priority && <Priority priority={this.props.priority}/>;
return (
<div class="headline">
<h2 class={"L"+this.props.level}
id={this.id}
>{/*onClick={()=>{this.toggle();}}*/}
{this.priority} {this.title} {!this.state.open && '...' }
</h2>
{/*<div class="tags"> {this.id}
{ this.tags }
</div>*/}
{this.state.open && children }
</div>
);
}
}
class timestamp extends Component {
render =()=>
<div class={this.props.class}>
{ this.props['year-start']}-{this.props['month-start']}-{this.props['day-start']}
</div>
}
// react won't call the right element if it's not uppercase
class Timestamp extends Component {
render =()=>
<div class={this.props.class}>
{ this.props['year-start']}-{this.props['month-start']}-{this.props['day-start']}
</div>
}
class planning extends Component {
constructor() {
super(...arguments);
let closed = this.props['closed'],
scheduled = this.props['scheduled'],
deadline = this.props['deadline'];
this.tag =
<div>
{ closed ? <div> Closed <Timestamp {...closed[1] }/></div> : '' }
{ scheduled ? <div> Scheduled <Timestamp {...scheduled[1]}/></div> : '' }
{ deadline ? <div> deadline <Timestamp {...deadline[1] }/></div> : '' }
</div>
}
render =()=> <div {...this.props}>{ this.tag}</div>
}
class item extends Component {
render =()=> <li {...this.props}>{this.props.children}</li>
}
const imageExtensionsRgx = new RegExp("(" + [
"png", "jpeg", "jpg", "gif", "svg", "bmp", "tiff",
"tif", "xbm", "xpm", "pbm", "pgm", "ppm", "svg"
].join("|") + ")$", "i");
const ImgExt = [
"png", "jpeg", "jpg", "gif", "svg", "bmp", "tiff",
"tif", "xbm", "xpm", "pbm", "pgm", "ppm", "svg",
"webm",
"mp3","ogg"
];
const MovieExt = ["webm"];
const AudioExt = [ "mp3", "ogg"];
const fileExt = ImgExt.concat(MovieExt.concat(AudioExt));
const FileExtRgx = new RegExp("(" + fileExt.join("|") + ")$", "i");
class link extends Component {
constructor() {
super(...arguments);
let type, link;
//if (!this.props['rawlink']) this.props['rawlink'] = this.link
// console.log(this.props['raw-link'])
this.rawlink = this.props['raw-link'];
this.link = this.linkMapping(this.props['type']);
if (this.link.startsWith('#')) {
this.anchortag = ()=>{
alert(this.link);
document.getElementById(this.link.slice(1)).scrollIntoView()
}
}
let ext = FileExtRgx.exec(this.link);
// handle image link
let ch = this.props.children;
if ( ext ) {
if ( ImgExt.includes(ext[0]) ) {
this.tag = <img src={this.link}/>
}
else if ( MovieExt.includes(ext[0]) ) {
this.tag = <Movie src={this.link}/>
}
else if ( AudioExt.includes(ext[0]) ) {
this.tag = <Audio src={this.link}/>
}
}
else {
let ch = this.props.children;
let innerlink = null;
if (ch && ch.length && ch[0] !== null) {
console.log(ch, typeof(ch[0]));
if((ch.length === 1) && ( typeof ch[0] === "string")) {
// check for an image link
let ext = FileExtRgx.exec(ch[0]);
if ( ext ) {
if ( ImgExt.includes(ext[0]) ) {
innerlink = <img src={ch[0]}/>
}
}
}
else
innerlink = renderList(ch);
}
else innerlink = this.rawlink;
this.tag = <a href={this.link}>{ innerlink} </a>
}
}
linkMapping(type) {
let rawlink = this.rawlink;
switch (this.props['type']) {
case 'http':
case 'https':
case 'file':
return rawlink;
case 'custom-id':
return rawlink;
case 'fuzzy':
return '/section/' + rawlink.replace(' ','%20');
default:
console.log('unknown type');
return rawlink
}
}
render =()=> this.tag
}
class target {
render =()=> <div id={this.props.value}/>
}
class Div extends Component{
postblanks() {
{Array(this.props['post-blank']).fill().map((_,i) => <br/>)}
}
}
class paragraph extends Div {
constructor() {
super(...arguments);
this.class = "paragraph "+ (this.props.attr_align?this.props.attr_align:'')
let name = this.props['name'];
if(name) {
this.id = name.startsWith('#') ? name.slice(1) : name
}
}
render() {
let name = this.props['name'];
//let time = new Date().toLocaleTimeString();
return <div class={this.class}
id={this.id = name && (name.startsWith('#') ? name.slice(1) : name)}>
{this.props.children}
{super.postblanks()}
</div>;
}
}
class SrcBlock extends Component {
static lang = {
python : 'py'
}
render =()=>
<div class="src-block">
<pre class="prettyprint">{this.props.value}</pre>
</div>
}
class table extends Div {
render =()=>
<table>
{this.props.children}
{super.postblanks()}
</table>
}
class TableRow extends Div {
render =()=>
<tr {...this.props}>
{this.props.children}
{super.postblanks()}
</tr>
}
class TableCell extends Div {
render =()=>
<td>
{this.props.children}
{super.postblanks()}
</td>
}
// or as function cls(x) + optional default
import styles from './styles';
export const CLS = {
...styles,
...{headline, keyword, timestamp, link, target,item, paragraph, planning,
'src-block':SrcBlock, table, 'table-row':TableRow, 'table-cell':TableCell
}};