Skip to content

Instantly share code, notes, and snippets.

@samuelgoto
Last active August 9, 2017 23:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save samuelgoto/d8d510032e93e773454eb2ad24fdb9a2 to your computer and use it in GitHub Desktop.
Save samuelgoto/d8d510032e93e773454eb2ad24fdb9a2 to your computer and use it in GitHub Desktop.

Introduction

This is an exploration of a DST to be embedded in javascript designed specifically to build and manipulate the HTML DOM.

Largely inspired by a {Kotlin, JFX, Protobuf, JSON-ish}-like syntax (to intermingle well with javascript code) and a JSX-like DOM building algorithm.

Basic idea

Introduce syntax to javascript to intermingle tree-like docs and code and vice versa. Something like this:

let doc = div {
  // comments inside document

  a         // child
  "order"   // child
  b         // child
  "matters" // child
  c         // child
  
  d(attribute: value) { // child
    e { // child's child
      f(g: false) // child's child's child with attribute
    }
  }
  
  // if-expressions
  if (cond) {
    a
  } else {
    b
  } 
  
  // for-expressions
  for (u in g) {
    a {
      b { `u.b` }
    }
  }
  
  // function calls and expressions
  var a = g();
  
  // inline callbacks
  div(onclick : function() { alert("hi"); }) {
    
  }
  
  
  // async document building
  await fetch("data.pb").map(u => {
    div {
      span {
        `u.b`
      }
    }
  })
};

Example

hello world

var doc = div {
  // New syntax introduced to build documents.
  div {
    // This is a text node and a child
    "hello world"
    
    // Also, comments allowed!!!
  }
};

attributes

var doc = div {
  form {
    "Enter your name:"
    input(enabled: false) {
    }
  }
};

callbacks

var doc = div {
  div {
     "hello world", 
     // inline callbacks
     onclick = function() {
        alert("hi");
      }
  }
};

expressions

var people = ["goto", "bnutter"];

var doc = div {
  // for-expressions enable you to iterate and add multiple nodes to the parent node.
  for (person in people) {
    div {
      p { a(href: `/users/{{person.id}}`) { `{person.name}` } },
    }
  },
  
  // arrow functions
  people.map(person => { div { p { `{{person.name}}`} })
  
  // try-catch expressions
  try { avatar(user); } catch {  { div { "invalid user id" }} },
  
  // TODO(goto): if-then-else expressions
  // TODO(goto): select operator, switch-like expressions

};

async-await

var doc = div {
  // async-await
  var users = await fetch("people.xml");
  users.map(user => div { `user.name` });
};

CSS Typed OM

var doc = div {
  div(
    // Uses CSS's TypedOM, 
    style: {
      width: "100%",
      position: "absolute"
    }) {
    "hello world"
  }
};

custom elements

// Doc-expressions can also be represented as classes that implement an Element interface.
// For example:

var doc = div {
  head {
   // CSS in JS!!!
   await fetch(["main.css-in-js", "hello.css-in-js"]).map(style => new Style(style));
  },
  body {
    bind ["goto", "bnutter"].map(user => new User(user)),

    // web-components like syntax, creating new node types!
    User {
      name: "Sam"
    }
  },
}

class Style implements Element {
  doc() {
    return div { "hello world" };
  }
}

class User implements Element {
  doc() {
    return div { "hello world" };
  }
}

Related Work

  • JSX
  • Kotlin typed builders
  • Elm
  • Hyperscript
  • json-ish
  • Om
  • Flutter
  • Anko layouts
  • Curl
  • JFX Script
  • JXON
  • E4X
@samuelgoto
Copy link
Author

@disnet does this sound like something I'd be able to prototype with sweet.js (if so, pointers on how to get started?)? Or does this go beyond its capabilities?

Asking because it seems like JSX was able to build a sweet.js thingy but it seems like they had to change the reader and the readtables:

https://facebook.github.io/jsx/

and

http://jlongster.com/Compiling-JSX-with-Sweet.js-using-Readtables

Any obvious feasibility constraints here?

@samuelgoto
Copy link
Author

samuelgoto commented Aug 1, 2017

Does {} conflict with object literals or with for-if-switch-{}-like statements? Would it be impossible to disambiguate?

NOTE(goto): how does JSX gets away with introducing <>-like syntax and not conflicting with < expressions?

If so, what other options do we have to disambiguate here?

Here are some ideas:

// doc {} keyword
doc {
  div {
    span {
      "hello world"
    }
  }
}

// double-curlies
div {{
  span {{
    "hello world"
  }}
}}


div {:
  span {:
    "hello world"
  :}
:}

// conflicts with <-expressions
div <
  span < 
    "hello world" 
  >
>

// smiley face: conflicts with method calls
div (:
  span (:
    "hello world"
  :)
:)

@samuelgoto
Copy link
Author

Kotlin builders:

import com.example.html.* // see declarations below

fun result(args: Array<String>) =
    html {
        head {
            title {+"XML encoding with Kotlin"}
        }
        body {
            h1 {+"XML encoding with Kotlin"}
            p  {+"this format can be used as an alternative markup to XML"}

            // an element with attributes and text content
            a(href = "http://kotlinlang.org") {+"Kotlin"}

            // mixed content
            p {
                +"This is some"
                b {+"mixed"}
                +"text. For more see the"
                a(href = "http://kotlinlang.org") {+"Kotlin"}
                +"project"
            }
            p {+"some text"}

            // content generated by
            p {
                for (arg in args)
                    +arg
            }
        }
    }

@samuelgoto
Copy link
Author

Groovy builders:

def company = builder.company(name: 'ACME') {
    address(id: 'a1', line1: '123 Groovy Rd', zip: 12345, state: 'JV')          
    employee(name: 'Duke', employeeId: 1, address: a1)                          
    employee(name: 'John', employeeId: 2 ){
      address( refId: 'a1' )                                                    
    }
}

@samuelgoto
Copy link
Author

https://msdn.microsoft.com/en-us/library/bb308959.aspx#linqoverview_topic7

querying language

using System;
using System.Linq;
using System.Collections.Generic;

class app {
  static void Main() {
    string[] names = { "Burke", "Connor", "Frank", 
                       "Everett", "Albert", "George", 
                       "Harris", "David" };

    IEnumerable<string> query = from s in names 
                               where s.Length == 5
                               orderby s
                               select s.ToUpper();

    foreach (string item in query)
      Console.WriteLine(item);
  }
}

@samuelgoto
Copy link
Author

samuelgoto commented Aug 2, 2017

more thoughts

Simple

function a() {
  return <div></div>;
}

function a() {
  return div {
    // hello world
  };
}

function a() {
  return __generic__("div", [], () => { 
    // hello world
  });
}

class Element {

}

function __generic__(name: string, args: Array<?>, init: Element => Array<Element>): Element {
  var el = new Element(arguments);
  var proxy = new Proxy(el, {
    get(target, key) {
      console.log(`accessing ${key} on ${target}`);

      // console.log(target.parent);
      if (target[key]) {
        return target[key];
      }
      
      var child = children.add(__generic__(key, ...));

      return child;
    }
  });
  init.call(proxy);
  return React.createElement("div", el.props, el.children);
}

function a() {
  return div {
    div {
    }
  }
}

function a() {
  return __generic__("div", [], () => {
    __generic__("div", [], () => {
    });
  });
}


function a() {
  return React.createElement("div", null);
}

Nested

function a() {
  return <div>
    <span></span>
    <span></span>
  </div>;
}

function a() {
  return div { 
    span {} 
    span {}
  }
}



ffunction a() {
  return React.createElement(
    "div",
    null,
    React.createElement("span", null),
    React.createElement("span", null)
  );
}

@samuelgoto
Copy link
Author

React implementation

class Element {
  constructor(name, args) {
    this.name = name;
    this.args = args;
    this.children = [];
  }

  addChild(el) {
    this.children.push(el);
  }
}

function __generic__(name, args, body) {
  console.log(`__generic__ ${name} ${args} ${body}`);
  let el = new Element(name, args);
  body.call(el);
  if (this instanceof Element) {
    console.log(`I have a parent!!`);
    this.addChild(el);
  } else {
    console.log(`I don't have a parent :(`);
    return el;
  }
}

let result = __generic__.call(this, "div", {foo: 1}, function() {
  console.log("am i an element?");
  console.log(this);
  __generic__.call(this, "span", {bar: 2}, function() {
    // hello world
  });
});

console.log(result);

@samuelgoto
Copy link
Author

// react

function buttonBar(x1,x2,x3){ 
  return
   <div>
     <button>{x1}</button>
     <button>{x2}</button>
     <button>{x3}</button>
   </div> 
}


// hyperscript
var h = require('hyperscript')
var obj = {
  a: 'Apple',
  b: 'Banana',
  c: 'Cherry',
  d: 'Durian',
  e: 'Elder Berry'
}
h('table',
  h('tr', h('th', 'letter'), h('th', 'fruit')),
  Object.keys(obj).map(function (k) {
    return h('tr',
      h('th', k),
      h('td', obj[k])
    )
  })
)

// JFX
 import javafx.stage.Stage;
 import javafx.scene.Scene;
 import javafx.scene.text.Text;
 import javafx.scene.text.Font;
 
 Stage {
     title: "Hello World"
     width: 250
     height: 80
     scene: Scene {
         content: Text {
             font : Font {
                 size : 24
             }
             x: 10, y: 30
             content: "Hello World"
         }
     } 
 }


// 

kotlin

// kotlin

fun main(args: Array<String>) {
    return
            html {
                head {
                    title { +"XML encoding with Kotlin" }
                }
                body {
                    h1 { +"XML encoding with Kotlin" }
                    p { +"this format can be used as an alternative markup to XML" }

                    // an element with attributes and text content
                    a(href = "http://jetbrains.com/kotlin") { +"Kotlin" }

                    // mixed content
                    p {
                        +"This is some"
                        b { +"mixed" }
                        +"text. For more see the"
                        a(href = "http://jetbrains.com/kotlin") { +"Kotlin" }
                        +"project"
                    }
                    p { +"some text" }

                    // content generated from command-line arguments
                    p {
                        +"Command line arguments were:"
                        ul {
                            for (arg in args)
                                li { +arg }
            }
                    }
                }
            }
}

@samuelgoto
Copy link
Author

// Technique #1 string building
let fragment = "";
fragment += "<div>";
fragment += "  <span>hello world</span>";
fragment += "</div>";
let node = document.createElement("div").innerHTML = fragment;
document.body.appendChild(node);

// Technique #2 imperative calls
let root = document.createElement("div");
let span = document.createElement("span");
let content = document.createTextNode("hello world");
span.appendChild(content);
root.appendChild(span);
document.body.appendChild(root);

// Technique #3: template languages
let root = mycomponent();
document.body.appendChild(root);

// mycomponent.soy, gets transpiled into JS
{template name="mycomponent"}
  hello world
{/template}

// Technique #4: DSL, extendeds JS
let root = 
  <div>
    <span>hello world</span>
  </div>;
document.body.appendChild(root);

let fragment = div {
  div {
    // New syntax introduced to build documents.
    div {
      // This is a text node and a child
      "hello world"    
      // Also, comments allowed!!!
    }
  }
};

document.body.write(fragment);

@samuelgoto
Copy link
Author

samuelgoto commented Aug 7, 2017

Some options for syntax for configuring a builder:

// Cast-like expression
let a = (HtmlElement) div {
  span {
  }
}

// Cast-like expression, no parens
let a = html div {
  span {
  }
}

"pragma"-like statement
let a = "html" div {
  span {
  }
}

// Root-level element
let a = html {
   div {
    span {
    }
  }
}

// decorator
let a = @html div {
  span {
  }
}

// #hash
let a = #html div {
  span {
  }
}

let a = div(@doc = html) {
  span {
  }
}

// Extra syntax
let a = doc(html) {
   div {
    span {
    }
  }
}

let a = html#div() {
  span {
  }
}

let a = div#html() {
  span {
  }
}

let a = div@html() {
  span {
  }
}

let a = div as html {
  span {
  }
}

let a = div[html] {
  span {
  }
}

let a = /** HtmlElement */ div {
  span {
  }
}

let a = new HtmlElement() {
  div {
    span {
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment