7d.nz

Org mode to JSX

[2017-05-26 Fri]

Β  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, [ {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
           }};