7d.nz

Org mode to JSX

[2017-05-26 ven.]

Β  react org

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, [ {propA:x, propsB:y,...} ], [ children...] ] β†’
   <node propA='x' propB='y'>  <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 =(a)=>  a?.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>
        );
    }
}

const Timestamp =(props)=>
      <div class={props.class}>
        { props['year-start']}-{props['month-start']}-{props['day-start']}
      </div>



const Planning =(props)=>
   <div {...this.props}>
     { props.closed    ? <div> closed    <Timestamp {...props.closed[1]   }/></div> : '' }
     { props.scheduled ? <div> scheduled <Timestamp {...props.scheduled[1]}/></div> : '' }
     { props.deadline  ? <div> deadline  <Timestamp {...props.deadline[1] }/></div> : '' }
   </div>


const Item =(props)=> <li {...props}>{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
       }};