├── .gitignore ├── demo ├── demo.gif ├── demo-2.gif ├── demo-3.gif ├── .vscode │ ├── settings.json │ └── tasks.json ├── build.hxml ├── Demo.hx └── index.html ├── src ├── nvd │ ├── Comp.hx │ ├── inner │ │ ├── CachedXML.hx │ │ ├── Utils.hx │ │ ├── HXX.hx │ │ ├── XMLComponent.hx │ │ ├── AObject.hx │ │ └── Tags.hx │ ├── Dt.hx │ └── Macros.hx └── Nvd.hx ├── release.sh ├── test ├── build.hxml ├── no-vdom.hxproj ├── bin │ └── index.html └── NvdTest.hx ├── haxelib.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dump 2 | *.js 3 | *.js.map -------------------------------------------------------------------------------- /demo/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R32/no-vdom/HEAD/demo/demo.gif -------------------------------------------------------------------------------- /demo/demo-2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R32/no-vdom/HEAD/demo/demo-2.gif -------------------------------------------------------------------------------- /demo/demo-3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/R32/no-vdom/HEAD/demo/demo-3.gif -------------------------------------------------------------------------------- /demo/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "haxe.displayConfigurations": [ 3 | ["build.hxml"] 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /demo/build.hxml: -------------------------------------------------------------------------------- 1 | -dce full 2 | -cp src 3 | -main Demo 4 | -lib no-vdom 5 | -D analyzer-optimize 6 | --macro exclude('haxe.iterators.ArrayIterator') 7 | -js demo.js -------------------------------------------------------------------------------- /src/nvd/Comp.hx: -------------------------------------------------------------------------------- 1 | package nvd; 2 | 3 | import js.html.DOMElement; 4 | 5 | extern abstract Comp(DOMElement) to DOMElement { 6 | inline function new( d : DOMElement ) this = d; 7 | } 8 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf release 3 | mkdir -p release 4 | cp -R -u src haxelib.json release 5 | chmod -R 777 release 6 | cd release 7 | zip -r release.zip ./ && mv release.zip ../ 8 | cd .. -------------------------------------------------------------------------------- /test/build.hxml: -------------------------------------------------------------------------------- 1 | -dce full 2 | -cp ../src 3 | -main NvdTest 4 | -lib csss 5 | # -debug 6 | -D analyzer-optimize 7 | -D message.reporting=classic 8 | -D message.no-color 9 | -js bin/test.js -------------------------------------------------------------------------------- /demo/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "haxe", 4 | "args": ["build.hxml"], 5 | "problemMatcher": "$haxe", 6 | "group": { 7 | "kind": "build", 8 | "isDefault": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "no-vdom", 3 | "classPath": "src", 4 | "version": "0.8.0", 5 | "url": "https://github.com/R32/no-vdom/", 6 | "license": "MIT", 7 | "tags": ["html"], 8 | "description": "A library similar to JSX", 9 | "releasenote": "Some improvements to make it simple", 10 | "dependencies": { 11 | "csss": "" 12 | }, 13 | "contributors": ["r32"] 14 | } -------------------------------------------------------------------------------- /src/nvd/inner/CachedXML.hx: -------------------------------------------------------------------------------- 1 | package nvd.inner; 2 | 3 | import csss.xml.Xml; 4 | import csss.xml.Parser; 5 | import sys.io.File; 6 | import sys.FileSystem; 7 | import haxe.macro.PositionTools.make in pmake; 8 | import haxe.macro.PositionTools.getInfos in pInfos; 9 | using nvd.inner.Utils; 10 | 11 | class CachedXML { 12 | 13 | var mtime: Float; 14 | 15 | public var xml(default, null) : Xml; 16 | 17 | function new() mtime = 0.; 18 | 19 | function update( path : String ) { 20 | var stat = FileSystem.stat(path); 21 | var cur = stat.mtime.getTime(); 22 | if (cur > mtime) { 23 | this.xml = Xml.parse( File.getContent(path) ); 24 | this.mtime = cur; 25 | } 26 | } 27 | 28 | public static function get( pexpr : haxe.macro.Expr ): CachedXML { 29 | var path = haxe.macro.ExprTools.getValue(pexpr); 30 | var ret = POOL.get(path); 31 | if (ret == null) { 32 | ret = new CachedXML(); 33 | POOL.set(path, ret); 34 | } 35 | try 36 | ret.update(path) 37 | catch( e : XmlParserException ) 38 | Nvd.fatalError(e.toString(), pmake({file: path, min: e.position, max: e.position + 1})) 39 | catch( e : Dynamic ) 40 | Nvd.fatalError(Std.string(e), pexpr.pos); 41 | return ret; 42 | } 43 | 44 | @:persistent static var POOL = new Map(); 45 | } -------------------------------------------------------------------------------- /demo/Demo.hx: -------------------------------------------------------------------------------- 1 | 2 | class Demo { 3 | static function main() { 4 | // hello world 5 | var hi = HelloWorld.ofSelector(".hello-world"); 6 | hi.text = "你好, 世界!"; 7 | trace(hi.text); 8 | 9 | // tick 10 | var tick = Tick.ofSelector(".tick"); 11 | tick.run(new haxe.Timer(1000)); 12 | 13 | // login 14 | var login = LoginForm.ofSelector("#login"); 15 | login.btn.onclick = function() { 16 | trace(login.getData()); 17 | } 18 | } 19 | } 20 | 21 | // tutorial hello world 22 | @:build(Nvd.build("index.html", ".hello-world", { 23 | text: $("h4").textContent, 24 | })) abstract HelloWorld(nvd.Comp) { 25 | } 26 | 27 | // tutorial tick 28 | @:build(Nvd.build("index.html", ".tick", { 29 | ts: $("span").textContent 30 | })) abstract Tick(nvd.Comp) { 31 | public inline function run(timer) { 32 | timer.run = function() { 33 | ts = "" + (Std.parseInt(ts) + 1); 34 | } 35 | } 36 | } 37 | 38 | // form 39 | @:build(Nvd.build("index.html", "#login", { 40 | btn: $("button[type=submit]"), 41 | name: $("input[name=name]").value, 42 | email: $("input[name=email]").value, 43 | remember: $("input[type=checkbox]").checked, // Note: IE8 does not support the pseudo-selector ":checked" 44 | herpderp: @:keep $("input[type=radio][name=herpderp]:checked").value, 45 | })) abstract LoginForm(nvd.Comp) { 46 | public inline function getData() { 47 | return { 48 | name: name, 49 | email: email, 50 | remember: remember, 51 | herpderp: herpderp, 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /test/no-vdom.hxproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | demo 7 | 19 | 20 | 21 |

no-vdom

22 |
23 |
24 | 25 |
26 |

hello world

27 |
28 | 29 | 30 |
31 |

tick 0

32 |
33 | 34 | 35 |
36 |

Forms

37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 | 45 |
46 |
47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /src/nvd/inner/Utils.hx: -------------------------------------------------------------------------------- 1 | package nvd.inner; 2 | 3 | using haxe.macro.PositionTools; 4 | import csss.xml.Xml; 5 | import csss.xml.Parser; 6 | import haxe.macro.Expr; 7 | 8 | class Utils { 9 | public static function string( e : Expr ) : String { 10 | return switch (e.expr) { 11 | case EConst(CString(s)): 12 | s; 13 | case EBinop(OpAdd, e1, e2): 14 | string(e1) + string(e2); 15 | default: 16 | Nvd.fatalError("Expected String", e.pos); 17 | } 18 | } 19 | public static function bool( e : Expr ) : Bool { 20 | return switch (e.expr) { 21 | case EConst(CIdent("true")): true; 22 | default: false; 23 | } 24 | } 25 | 26 | public static function doParse( markup : Expr ) : Xml { 27 | return try { 28 | var txt = switch(markup.expr) { 29 | case EConst(CString(s)): s; 30 | case EMeta({name: ":markup"}, macro $v{ (s : String) }): s; 31 | default: 32 | throw "Expected String"; 33 | } 34 | Xml.parse(txt); 35 | } catch (e : XmlParserException) { 36 | var pos = markup.pos.getInfos(); 37 | pos.min += e.position; 38 | pos.max = pos.min + 1; 39 | Nvd.fatalError(e.toString(), pos.make()); 40 | } catch (e) { 41 | Nvd.fatalError(Std.string(e), markup.pos); 42 | } 43 | } 44 | 45 | static public function isSVG( node : Xml ) : Bool { 46 | while(node != null) { 47 | if (node.nodeName == "svg") 48 | return true; 49 | node = node.parent; 50 | } 51 | return false; 52 | } 53 | 54 | static public function punion( p1 : Position, p2 : Position ) { 55 | var pos = p1.getInfos(); 56 | pos.max = p2.getInfos().max; // ???do max(p1.max, p2.max), 57 | return pos.make(); 58 | } 59 | 60 | // if a.b.c then get c.position 61 | static public function pfield( full : Position, left : Position ) { 62 | var pos = full.getInfos(); 63 | pos.min = left.getInfos().max + 1; // ".".length == 1; 64 | return pos.make(); 65 | } 66 | 67 | // Simply compare the names of XML 68 | static public function simplyCompare( x1 : Xml, x2 : Xml ) : Bool { 69 | if (x1 == null || x2 == null || x1.nodeName != x2.nodeName) 70 | return false; 71 | var c1 = @:privateAccess x1.children.filter( x -> x.nodeType == Element ); 72 | var c2 = @:privateAccess x2.children.filter( x -> x.nodeType == Element ); 73 | if (c1.length != c2.length) 74 | return false; 75 | for (i in 0...c1.length) { 76 | if (!simplyCompare(c1[i], c2[i])) 77 | return false; 78 | } 79 | return true; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/bin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | test 7 | 11 | 12 | 13 |

It works!

14 | some word 15 |
18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 | text 26 |
27 | 28 |
29 |

Lorem ipsum dolor sit

30 |

amet consectetuer hi 31 |

32 |
33 |
34 |

p1 1 p1 2 p1 3

35 |

p2 1 p2 2 p2 3

36 |

p3 1 p3 2 p3 3

37 |
38 | 39 |
40 |
41 | previousSibling 42 | Element 43 | nextSibling 44 |
45 | 46 | 47 | 48 | You are my fire! 49 | 50 | 51 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/Nvd.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | #if macro 4 | using nvd.inner.Utils; 5 | import nvd.inner.Tags; 6 | import nvd.inner.CachedXML; 7 | import nvd.inner.XMLComponent; 8 | using csss.Query; 9 | import csss.xml.Xml; 10 | import haxe.macro.Expr; 11 | import haxe.macro.Context; 12 | #end 13 | class Nvd { 14 | /* 15 | Uses `{{` `}}` as variable separator. 16 | 17 | ```hx 18 | var title = "hi there"; 19 | var content = "click here"; 20 | var fn = function(){ return "string"; } 21 | var div = HXX( 22 |
23 | LL {{ content }} RR 24 |
25 | {{ fn() }} 26 |
27 | ); 28 | document.body.appendChild(div); 29 | ``` 30 | */ 31 | macro public static function HXX( markup : Expr ) { 32 | var comp = XMLComponent.fromMarkup(markup, true); 33 | var expr = comp.parse(); 34 | expr.pos = markup.pos; 35 | return expr; 36 | } 37 | #if macro 38 | /** 39 | since you can't build "macro function" from macro, So you need to write it manually: 40 | 41 | ```hx 42 | // Note: This method should be placed in "non-js" class 43 | macro public static function one(selector) { 44 | return Nvd.buildQuerySelector("bin/index.html", selector); 45 | } 46 | ``` 47 | */ 48 | public static function buildQuerySelector( path : String, sexpr : ExprOf) : Expr { 49 | var pos = Context.currentPos(); 50 | var cha = CachedXML.get({expr: EConst(CString(path)), pos : pos}); 51 | var css = sexpr.string(); 52 | var node = cha.xml.querySelector(css); 53 | if (node == null) 54 | fatalError('Could not find: "$css" in $path', sexpr.pos); 55 | var ctype = Tags.ctype(node.nodeName, node.isSVG() , false); 56 | return macro @:pos(pos) (cast nvd.Dt.Docs.querySelector($sexpr): $ctype); 57 | } 58 | 59 | /* 60 | Syntax: `refname : $(css-selector, ?keep-css)[.xxx]` 61 | 62 | example: Only 4 types of references are supported. (Element|Property|Attribute|Style) 63 | 64 | ```hx 65 | Nvd.build("file/to/index.html", ".template-1", { 66 | link : $("a"), // Element 67 | text : $("p").textContent, // Property 68 | title : $("a").title, // Property 69 | atit : $("a").attr.title, // Attribute, OR .attr["title"] 70 | cls : $("a").className, // Property 71 | x : $(null).style.offsetLeft // Style 72 | display : $(null).style.display // Style 73 | }) abstract Foo(nvd.Comp) {} 74 | // .... 75 | var foo = new Foo(); 76 | foo.| 77 | ``` 78 | */ 79 | public static function build( pexpr : ExprOf, sexpr : ExprOf, ?defs, isSVG = false ) { 80 | var path = pexpr.string(); 81 | var css = sexpr.string(); 82 | var cha = CachedXML.get(pexpr); 83 | var top = cha.xml.querySelector(css); 84 | if (top == null) 85 | fatalError('Could not find: "$css" in $path', sexpr.pos); 86 | var comp = new XMLComponent(path, 0, top, css, isSVG, false); 87 | return nvd.Macros.make(comp, defs); 88 | } 89 | 90 | public static function buildString( e : Expr, ?defs) { 91 | var comp = XMLComponent.fromMarkup(e, false); 92 | return nvd.Macros.make(comp, defs); 93 | } 94 | 95 | static inline var ERR_PREFIX = "[no-vdom]: "; 96 | 97 | static public function fatalError(msg, pos) : Dynamic return Context.fatalError(ERR_PREFIX + msg, pos); 98 | #end 99 | } 100 | -------------------------------------------------------------------------------- /test/NvdTest.hx: -------------------------------------------------------------------------------- 1 | package; 2 | 3 | import js.Browser.document; 4 | import js.Browser.console; 5 | import Nvd.HXX; 6 | 7 | class NvdTest { 8 | static function test_markup() { 9 | var link = "variable link"; 10 | var haha = "haha"; 11 | var div = HXX( 12 |
13 | 14 | 15 |
16 | the {{link}} 1 17 |
18 | the {{link}} 2 19 |
20 |
    21 |
  1. 1
  2. 22 |
  3. 2
  4. 23 |
  5. 3
  6. 24 |
  7. 4
  8. 25 |
26 |
27 | ); 28 | document.body.appendChild(div); 29 | var link = HXX(here); 30 | var col = []; 31 | for (i in 0...Std.random(20)) 32 | col.push(HXX(
  • n : {{ i }}
  • )); 33 | 34 | var ul = HXX(
      click {{ link }} {{ col }}
    ); 35 | document.body.appendChild(ul); 36 | 37 | var css = "body { margin : 0; }"; 38 | var style = HXX( ); 39 | document.head.appendChild(style); 40 | } 41 | 42 | static function test_comp() { 43 | var foo = new Foo(document.querySelector("div.flex-table")); 44 | foo.value = "a b c"; 45 | foo.title = "Greeting"; 46 | trace(foo.value); 47 | trace(foo.title); 48 | 49 | var bar: Bar = Bar.ofSelector("div.template-1"); 50 | trace(bar.x); 51 | trace(bar.y); 52 | trace(bar.text); 53 | bar.text = "abc"; 54 | trace(bar.textNode.nodeValue); 55 | 56 | var testSVG = TestSVG.ofSelector("#testSVG"); 57 | 58 | // buildString 59 | var tee = Tee.create(); 60 | document.body.appendChild(tee); 61 | document.body.appendChild(Bar.create()); 62 | 63 | // svg element 64 | var lyrics = [ 65 | "you are my fire", 66 | "the one desire", 67 | "Believe when I say", 68 | "I want it that way", 69 | ]; 70 | var lyricsIndex = 0; 71 | testSVG.rectOnclick = function(e) { 72 | trace(testSVG.text); 73 | lyricsIndex = (lyricsIndex + 1) % lyrics.length; 74 | testSVG.text = lyrics[lyricsIndex]; 75 | } 76 | 77 | var text = Depth.ofSelector(".depth").selected; 78 | trace(text, text == "p3 2"); 79 | } 80 | 81 | static function main() { 82 | var text = "Click here"; 83 | var a = HXX({{ text }}); 84 | document.body.appendChild(a); 85 | 86 | var title = "hi there"; 87 | var content = "click here"; 88 | var div = HXX(
    LL {{ content }} RR
    ); 89 | document.body.appendChild(div); 90 | 91 | test_markup(); 92 | test_comp(); 93 | } 94 | 95 | static inline var RED = "red"; 96 | static inline var color = RED; 97 | } 98 | 99 | @:build(Nvd.build("bin/index.html", "div.flex-table", { 100 | display: $(null).style.display, 101 | input: $(".input-block"), 102 | value: @:keep $(".input-block").value, 103 | title: $("[title]").attr["title"], 104 | sub: ($(".flex-table-item-primary") : FooChild), 105 | })) abstract Foo(nvd.Comp) { 106 | } 107 | 108 | @:build(Nvd.build("bin/index.html", "div.flex-table-item-primary", { 109 | })) abstract FooChild(nvd.Comp) { 110 | } 111 | 112 | @:build(Nvd.build("bin/index.html", "div.template-1", { 113 | link: $("a"), 114 | text: $("p").innerText, // text is custom property 115 | title: $("a").attr.title, 116 | cls: $("a").className, 117 | x: $(null).offsetLeft, 118 | y: $("").offsetTop, 119 | textNode: $("a").previousSibling, 120 | })) abstract Bar(nvd.Comp) { 121 | } 122 | 123 | // svg element 124 | @:build(Nvd.build("bin/index.html", "#testSVG", { 125 | rectOnclick: $("rect").onclick, 126 | text: $("text").textContent, 127 | }, true)) abstract TestSVG(nvd.Comp) { 128 | public function new( dom : js.html.DOMElement ) { 129 | this = cast dom; 130 | } 131 | } 132 | 133 | 134 | @:build(Nvd.build("bin/index.html", ".depth", { 135 | selected : $("[selected]").innerText, 136 | })) extern abstract Depth(nvd.Comp) { 137 | } 138 | 139 | 140 | // buildString 141 | @:build(Nvd.buildString( 142 |
    143 | 144 | 145 |
    )) abstract Tee(nvd.Comp) { 146 | } 147 | -------------------------------------------------------------------------------- /src/nvd/Dt.hx: -------------------------------------------------------------------------------- 1 | package nvd; 2 | 3 | import js.html.DOMElement; 4 | 5 | /** 6 | DOM Tools 7 | */ 8 | @:native("dt") class Dt { 9 | 10 | public static function setAttr( dom : DOMElement, name : String, value : String ) : String { 11 | if (value == null) 12 | dom.removeAttribute(name); 13 | else 14 | dom.setAttribute(name, value); 15 | return value; 16 | } 17 | 18 | public static function getText( dom : DOMElement ) : String { 19 | if (dom.nodeType == js.html.Node.ELEMENT_NODE) { 20 | switch (dom.tagName) { 21 | case "INPUT": 22 | return (cast dom).value; 23 | case "OPTION": 24 | return (cast dom).text; 25 | case "SELECT": 26 | var select: js.html.SelectElement = cast dom; 27 | return (cast select.options[select.selectedIndex]).text; 28 | default: 29 | return dom.innerText; 30 | } 31 | } else if (dom.nodeType != js.html.Node.DOCUMENT_NODE) { 32 | return dom.nodeValue; 33 | } 34 | return null; 35 | } 36 | 37 | public static function setText( dom : DOMElement, text : String) : String { 38 | if (dom.nodeType == js.html.Node.ELEMENT_NODE) { 39 | switch (dom.tagName) { 40 | case "INPUT": 41 | (dom: Dynamic).value = text; 42 | case "OPTION": 43 | (dom: Dynamic).text = text; 44 | case "SELECT": 45 | var select: js.html.SelectElement = cast dom; 46 | for (i in 0...select.options.length) { 47 | if ((cast select.options[i]).text == text) { 48 | select.selectedIndex = i; 49 | break; 50 | } 51 | } 52 | default: 53 | dom.innerText = text; 54 | } 55 | } else if (dom.nodeType != js.html.Node.DOCUMENT_NODE) { // #text, #comment 56 | dom.nodeValue = text; 57 | } 58 | return text; 59 | } 60 | 61 | public static function getCss( dom : DOMElement, name : String ) : String { 62 | if ((cast dom).currentStyle) { 63 | return (cast dom).currentStyle[cast name]; 64 | } else { 65 | return js.Browser.window.getComputedStyle(cast dom, null).getPropertyValue(name); 66 | } 67 | } 68 | 69 | public static function setStyle( dom : DOMElement, styles : haxe.DynamicAccess ) : Void { 70 | js.Syntax.code("for(var k in {0}) {1}[k] = {0}[k]", styles, dom.style); 71 | } 72 | } 73 | 74 | /* 75 | * `lookup(elem, 1, 2, 3)` is similar to `elem.children[1].children[2].children[3]` 76 | */ 77 | #if (js_es < 6) 78 | @:native("__hcr") function lookup( node : js.html.Node ) : Any { 79 | // js-es5 will generate ugly haxe.Rest code 80 | var path : Array = js.Syntax.code("arguments"); 81 | var i = 1; 82 | #else 83 | @:native("__hcr") function lookup( node : js.html.Node, path : haxe.Rest ) : Any { 84 | var i = 0; 85 | #end 86 | var n = path.length; 87 | while (i < n) { 88 | node = node.firstChild; 89 | var s = path[i++]; 90 | var t = 0; 91 | while ( (node : Dynamic) ) { 92 | if (node.nodeType == js.html.Node.ELEMENT_NODE && s == t++) 93 | break; 94 | node = node.nextSibling; 95 | } 96 | } 97 | return node; 98 | } 99 | 100 | /* 101 | * It's used automatically by the macro 102 | */ 103 | @:native("__h") @:pure function h( name : String, ?attr : haxe.DynamicAccess, ?sub : Dynamic ) : DOMElement { 104 | var dom = Docs.createElement(name); 105 | if (attr != null) { 106 | js.Syntax.code("for(var k in {0}) {1}.setAttribute(k, {0}[k])", attr, dom); 107 | } 108 | hrec(dom, sub, false); 109 | return dom; 110 | } 111 | 112 | @:native("__hrec") private function hrec( box : js.html.DOMElement, sub : Dynamic, loop : Bool ) { 113 | if (sub == null) 114 | return; 115 | if (sub is Array) { 116 | var i = 0; 117 | var len = sub.length; 118 | while (i < len) { 119 | hrec(box, sub[i], true); 120 | ++ i; 121 | } 122 | } else if(js.Syntax.typeof(sub) == "object") { // js.html.DOMElement or js.html.Text 123 | box.appendChild(sub); 124 | } else if (loop) { 125 | box.appendChild(Docs.createTextNode(sub)); 126 | } else { 127 | box.innerText = sub; 128 | } 129 | } 130 | 131 | /* 132 | * Used to prevent the optimizer to doing "const propagation" for "String" 133 | */ 134 | @:semantics(variable) 135 | extern abstract VarString(String) to String from String { 136 | } 137 | 138 | /* 139 | * Copied several "pure" functions for internal macros 140 | */ 141 | @:pure 142 | @:noCompletion 143 | @:native("document") 144 | extern class Docs { 145 | static function createElement( localName : String ) : js.html.Element; 146 | static function querySelector( data : String ) : js.html.Element; 147 | static function createTextNode( data : String ) : js.html.Text; 148 | } 149 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | no-vdom 2 | -------- 3 | 4 | A haxelib used for static(in compile time) HTML data building 5 | 6 | ## Installation 7 | 8 | ```bash 9 | haxelib install no-vdom 10 | ``` 11 | 12 | ## Feature 13 | 14 | * Intelligent: 15 | 16 | ![screen shot](demo/demo-3.gif) 17 | 18 | * **Zero Performance Loss**, Zero runtime dependency 19 | 20 | * IE8+ Support. 21 | 22 | ### HXX 23 | 24 | Uses `{{` `}}` as variable separator. 25 | 26 | ```haxe 27 | var title = "hi there"; 28 | var content = "click here"; 29 | var div = HXX(); 30 | document.body.appendChild(div); 31 | ``` 32 | 33 | Generated js: 34 | 35 | ```js 36 | document.body.appendChild(__h("div",null,__h("a",{ title : "hi there"}," LL " + "click here" + " RR "))); 37 | ``` 38 | 39 | If expr with prefix `$` inside `{{ }}` then that will be explicitly treated as `Element/Array`. 40 | 41 | ```haxe 42 | var link = HXX(here); 43 | var col = []; 44 | for (i in 0...Std.random(20)) 45 | col.push(HXX(
  • n : {{ i }}
  • )); 46 | 47 | var ul = HXX(
      click {{ $link }} {{ $col }}
    ); // Explicitly mark variable as "Element" 48 | var ul = HXX(
      click {{ link }} {{ col }}
    ); // Auto-sensing type, powered by haxe macro 49 | document.body.appendChild(ul); 50 | ``` 51 | 52 | Generated js: 53 | 54 | ```js 55 | var link = __h("a",null,"here"); 56 | var col = []; 57 | var _g = 0; 58 | var _g1 = Std.random(20); 59 | while(_g < _g1) col.push(__h("li",null,"n : " + _g++)); 60 | document.body.appendChild(__h("ul",null,[" click ",link,col])); 61 | ``` 62 | 63 | ### data binding 64 | 65 | ..... 66 | 67 | #### Syntax 68 | 69 | ```haxe 70 | /** 71 | @file: Specify an HTML file, Relative to current project. 72 | @root: a css selector which will be queried as **root DOMElement** for the Component. 73 | @defs: Specify property binding, for example: 74 | { 75 | btn : $("button"), 76 | text: $("label").textContext, 77 | value: $("input[type=button]").attr.value, 78 | x: $(null).style.left, // $(null) is self 79 | y: $(null).style.top, 80 | } 81 | */ 82 | Nvd.build(file: String, root: String, ?defs: Dynamic); 83 | 84 | /** 85 | @selector: a css selector that used to specify a child DOMElement , if null it's represented as root DOMElement. 86 | */ 87 | function $(selector:String) : AUTO; 88 | 89 | /** 90 | There are 4 types available: 91 | $(...) , // DOMElement 92 | $(...).XXX , // Property 93 | $(...).attr.XXX , // Attribute, 94 | $(...).attr["XXX"] , // Attribute, 95 | $(...).style.XXX , // Style-Property 96 | 97 | // Extended syntax 98 | @:keep $("selector").XXX , // If "@:keep" then "selector" will be retained to output/runtime. 99 | ($("selector") : OtherType), // Explicitly type casting to OtherType 100 | */ 101 | ``` 102 | 103 | sample: 104 | 105 | ```html 106 |
    107 |
    108 | 109 | 110 |
    111 |
    112 | 113 | 114 |
    115 |
    116 | 117 | 118 | 119 | 120 |
    121 |
    122 | ``` 123 | 124 | Component: 125 | 126 | ```hx 127 | @:build(Nvd.build("index.html", "#login", { 128 | btn: $("button[type=submit]"), 129 | name: $("input[name=name]").value, 130 | email: $("input[name=email]").value, 131 | remember: $("input[type=checkbox]").checked, 132 | herpderp: @:keep $("input[type=radio][name=herpderp]:checked").value, 133 | })) abstract LoginForm(nvd.Comp) { 134 | public inline function getData() { 135 | return { 136 | name: name, 137 | email: email, 138 | remember: remember, 139 | herpderp: herpderp, 140 | } 141 | } 142 | } 143 | 144 | 145 | class Demo { 146 | static function main() { 147 | // login 148 | var login = LoginForm.ofSelector("#login"); 149 | login.btn.onclick = function() { 150 | trace(login.getData()); 151 | } 152 | } 153 | } 154 | ``` 155 | 156 | ![screen shot](demo/demo.gif) 157 | 158 | ![screen shot](demo/demo-2.gif) 159 | 160 | output: 161 | 162 | ```js 163 | // Generated by Haxe 4.0.0 (git build development @ e6f3b7d) 164 | (function () { "use strict"; 165 | var Demo = function() { }; 166 | Demo.main = function() { 167 | var login = window.document.querySelector("#login"); 168 | login.children[2].children[3].onclick = function() { 169 | console.log("Demo.hx:9:",{ name : login.children[0].children[1].value, email : login.children[1].children[1].value, remember : login.children[2].children[0].children[0].checked, herpderp : login.querySelector("input[type=radio][name=herpderp]:checked").value}); 170 | }; 171 | }; 172 | Demo.main(); 173 | })(); 174 | ``` 175 | 176 | ## CHANGES 177 | 178 | * `0.8.0` : 179 | - Some improvements to make it simple. 180 | - Added explicit type casting. e.g: `($("selector") : TabComponent)` 181 | - [HXX] simple markup will be automatically inlined. 182 | * `0.5.0.`: 183 | - Added Simple `HXX` 184 | - Added SVG elements support(Only for Query) 185 | -------------------------------------------------------------------------------- /src/nvd/inner/HXX.hx: -------------------------------------------------------------------------------- 1 | package nvd.inner; 2 | 3 | using StringTools; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Type; 6 | import haxe.macro.Context; 7 | import haxe.macro.PositionTools; 8 | 9 | private enum State { 10 | TEXT; 11 | EXPR; 12 | } 13 | 14 | class HXX { 15 | 16 | var skip : Bool; 17 | 18 | public function new( isHXX : Bool) { 19 | skip = !isHXX; 20 | WhatMode.init(); 21 | } 22 | 23 | public function parse( s : String, pos : Position ) { 24 | var ret = {expr: EConst(CString(s)), pos: pos}; 25 | if (skip) 26 | return ret; 27 | var col = []; 28 | var pinfo = PositionTools.getInfos(pos); 29 | inline function phere(i, len) return PositionTools.make({file: pinfo.file, min: pinfo.min + i, max: pinfo.min + i + len}); 30 | inline function PUSH(s) col.push(s); 31 | var i = 0; 32 | var len = s.length - 1; // since }} %} needs 2 char. 33 | var start = 0; 34 | var STATE = TEXT; 35 | var width = 0; 36 | while (i < len) { 37 | var c = s.fastCodeAt(i++); 38 | switch (STATE) { 39 | case TEXT if (c == "{".code): 40 | c = s.fastCodeAt(i++); 41 | if (c != "{".code) 42 | continue; 43 | width = i - 2 - start; 44 | if (width > 0) { 45 | var sub = s.substr(start, width); 46 | PUSH({expr: EConst(CString(sub)), pos: phere(start, width)}); 47 | } 48 | start = i; 49 | STATE = EXPR; 50 | case EXPR if (c == "}".code): // {{ expr }} 51 | c = s.fastCodeAt(i++); 52 | if (c != "}".code) 53 | continue; 54 | start = leftTrim(s, start, i - 2 - start); // i - 2 - old-start 55 | width = rightTrim(s, start, i - 2 - start); // i - 2 - new-start 56 | if (width > 0) { 57 | var sub = s.substr(start, width); 58 | PUSH(Context.parse(sub, phere(start, width))); 59 | } 60 | start = i; 61 | STATE = TEXT; 62 | default: 63 | } 64 | } 65 | if (STATE != TEXT) 66 | Nvd.fatalError("Expected }", pos); 67 | width = len + 1 - start; 68 | if (width > 0) 69 | PUSH( {expr: EConst(CString( s.substr(start, width) )), pos: phere(start, width)} ); 70 | return switch(col.length) { 71 | case 0: 72 | null; 73 | case 1: 74 | col[0]; 75 | default: 76 | concat(col); 77 | } 78 | } 79 | 80 | function concat( col : Array ) { 81 | var group = []; 82 | var prev = null; 83 | function APPEND(e) { 84 | if (prev != null) { 85 | group.push(prev); 86 | prev = null; 87 | } 88 | group.push(e); 89 | } 90 | for (e in col) { 91 | switch (e.expr) { 92 | case EConst(CIdent(s)) if (s.charCodeAt(0) == "$".code): 93 | APPEND(macro @:pos(e.pos) $i{s.substr(1, s.length - 1)}); 94 | continue; 95 | default: 96 | var mode = WhatMode.detects(e); 97 | if (mode != TCString) { 98 | APPEND(e); 99 | continue; 100 | } 101 | } 102 | if (prev != null) { 103 | prev = {expr: EBinop(OpAdd, prev, e), pos : e.pos}; 104 | } else { 105 | prev = e; 106 | } 107 | } 108 | if (prev != null) 109 | group.push(prev); 110 | return switch(group.length) { 111 | case 0: 112 | null; 113 | case 1: 114 | group[0]; 115 | default: 116 | var pos = Utils.punion(group[0].pos, group[group.length-1].pos); 117 | macro @:pos(pos) $a{group}; 118 | } 119 | } 120 | 121 | function leftTrim( s : String, start : Int, len : Int ) : Int { 122 | var max = start + len; 123 | while(start < max) { 124 | var c = s.fastCodeAt(start); 125 | if ( !(c == " ".code || (c > 8 && c < 14)) ) 126 | break; 127 | start++; 128 | } 129 | return start; 130 | } 131 | 132 | function rightTrim( s : String, start : Int, len : Int ) : Int { 133 | var i = start + len - 1; 134 | while(i >= start) { 135 | var c = s.fastCodeAt(i); 136 | if ( !(c == " ".code || (c > 8 && c < 14)) ) 137 | break; 138 | i--; 139 | } 140 | return i + 1 - start; 141 | } 142 | } 143 | 144 | class WhatMode { 145 | 146 | static var types : { node : Type, string : Type, simples : EReg }; 147 | 148 | public static function init() { 149 | types = { 150 | node : Context.getType("js.html.Node"), 151 | string : Context.getType("String"), 152 | simples : ~/^(Int|Float|Boolean)$/, 153 | } 154 | } 155 | 156 | static function isBlock( e : Expr ) { 157 | return switch(e.expr) { 158 | case EParenthesis(e), ECast(e, _), ECheckType(e, _), EMeta(_, e): 159 | isBlock(e); 160 | case EBlock(_), EIf(_), EFor(_), EWhile(_), ESwitch(_), ETry(_), ETernary(_): 161 | true; 162 | case ECall(macro nvd.Dt.h, _): 163 | true; 164 | default: 165 | false; 166 | } 167 | } 168 | 169 | static function isDynamic( t : Type, e : Expr ) { 170 | return switch(t) { 171 | case TDynamic(_): 172 | true; 173 | case TMono(_): 174 | Nvd.fatalError("Unknown: " + haxe.macro.ExprTools.toString(e), e.pos); 175 | default: 176 | false; 177 | } 178 | } 179 | 180 | public static function detects( e : Expr ) : ContentMode { 181 | if (e == null) 182 | return TCNull; 183 | if (isBlock(e)) 184 | return TCComplex; 185 | // fast detects 186 | switch(e.expr) { 187 | case EConst(CIdent("null")): 188 | return TCString; 189 | case EConst(CIdent(_)): 190 | case EConst(_), EBinop(OpAdd, _, _): 191 | return TCString; 192 | default: 193 | } 194 | // do unify 195 | var mode = TCComplex; 196 | try { 197 | var t = Context.follow(Context.typeof(e)); 198 | if (isDynamic(t, e)) { 199 | } else if (Context.unify(t, types.node)) { 200 | mode = TCNode; 201 | } else if (Context.unify(t, types.string)) { 202 | mode = TCString; 203 | } else if (types.simples.match( haxe.macro.TypeTools.toString(t) )) { 204 | mode = TCString; 205 | } else { 206 | 207 | } 208 | } catch (_) { 209 | } 210 | return mode; 211 | } 212 | } 213 | 214 | enum ContentMode { 215 | TCString; 216 | TCNode; 217 | TCComplex; 218 | TCNull; 219 | } 220 | -------------------------------------------------------------------------------- /src/nvd/Macros.hx: -------------------------------------------------------------------------------- 1 | package nvd; 2 | 3 | #if macro 4 | using haxe.macro.Tools; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type; 7 | import haxe.macro.Context; 8 | 9 | import nvd.inner.XMLComponent; 10 | import nvd.inner.AObject; 11 | import nvd.inner.Tags; 12 | import Nvd.fatalError; 13 | 14 | @:allow(Nvd) 15 | class Macros { 16 | static function make(comp: XMLComponent, defs: Expr): Array { 17 | var pos = Context.currentPos(); 18 | var cls : ClassType = Context.getLocalClass().get(); 19 | var cls_path = switch (cls.kind) { 20 | case KAbstractImpl(_.get() => c): 21 | if (c.type.toString() != "nvd.Comp") 22 | fatalError('Only for abstract ${c.name}(nvd.Comp) ...', pos); 23 | {pack: c.pack, name: c.name}; 24 | default: 25 | fatalError('Only for abstract type', pos); 26 | } 27 | var fields = Context.getBuildFields(); 28 | var reserve = new Map(); 29 | for (f in fields) 30 | reserve.set(f.name, true); 31 | 32 | var aobj = new AObject(comp); 33 | try { 34 | aobj.parse(defs); 35 | } catch ( e : AObjectError ) { 36 | fatalError(e.msg, e.pos); 37 | } 38 | 39 | // haxe#12005 "_new" => "_hx_new" (abstract constructor) 40 | if (!(reserve.exists("_new") || reserve.exists("_hx_new"))) { 41 | var ct_dom = macro :js.html.DOMElement; 42 | fields.push({ 43 | name: "new", 44 | access: [APublic, AInline], 45 | pos: pos, 46 | kind: FFun({ 47 | args: [{name: "d", type: ct_dom}], 48 | ret: null, 49 | expr: macro this = cast (d: $ct_dom), // type checking and casting 50 | }) 51 | }); 52 | } 53 | var ct_top = comp.topComplexType(); 54 | fields.push({ 55 | name: "dom", 56 | access: [APublic], 57 | pos: pos, 58 | kind: FProp("get", "never", ct_top) 59 | }); 60 | fields.push({ 61 | name: "get_dom", 62 | access: [AInline, APrivate], 63 | pos: pos, 64 | meta: [{name: ":to", pos: pos}], 65 | kind: FFun({ 66 | args: [], 67 | ret: ct_top, 68 | expr: macro return cast this 69 | }) 70 | }); 71 | var ct_cls = TPath(cls_path); 72 | if (!reserve.exists("ofSelector")) { 73 | fields.push({ 74 | name: "ofSelector", 75 | access: [APublic, AInline, AStatic], 76 | pos: pos, 77 | kind: FFun({ 78 | args: [{name: "s", type: macro :String}], 79 | ret: ct_cls, 80 | expr: macro return (cast nvd.Dt.Docs.querySelector(s) : $ct_cls) 81 | }) 82 | }); 83 | } 84 | if (!comp.isSVG && !reserve.exists("create")) { 85 | var ecreate = comp.parse(); 86 | ecreate = {expr: ECast(ecreate, null), pos: pos}; 87 | fields.push({ 88 | name: "create", 89 | access: [APublic, AInline, AStatic], 90 | pos: pos, 91 | kind: FFun({ 92 | args: [], 93 | ret: ct_cls, 94 | expr: macro return $ecreate 95 | }) 96 | }); 97 | } 98 | if (comp.selector != null) { 99 | fields.push({ 100 | name: "SELECTOR", 101 | access: [APublic, AInline, AStatic], 102 | pos: pos, 103 | kind : FVar(null, macro $v{ comp.selector }) 104 | }); 105 | } 106 | var js_es6 = Std.parseInt(Context.definedValue("js_es")) > 5; 107 | for (k in aobj.bindings.keys()) { 108 | var item = aobj.bindings.get(k); 109 | var aname = item.name; 110 | var edom = if (item.keepCSS && item.markup.css != null) { 111 | macro cast dom.querySelector($v{item.markup.css}); 112 | } else { 113 | if (item.markup.path.length < 2) { 114 | htmlChildren(item.markup.path, item.markup.pos); 115 | } else { 116 | var args : Array = [macro this]; 117 | for (i in item.markup.path) 118 | args.push({expr : EConst(CInt(i + "")), pos : item.markup.pos}); 119 | if (js_es6) { 120 | macro nvd.Dt.lookup($a{ args }); 121 | } else { 122 | macro (nvd.Dt.lookup : haxe.Constraints.Function)($a{ args }); 123 | } 124 | } 125 | } 126 | var edom = { 127 | expr: ECheckType(edom, item.markup.ctype), 128 | pos : item.markup.pos 129 | }; 130 | fields.push({ 131 | name: k, 132 | access: k.charCodeAt(0) == "_".code ? [APrivate] : [APublic], 133 | kind: FProp("get", (item.readOnly ? "never": "set"), item.ctype), 134 | pos: item.markup.pos, 135 | }); 136 | fields.push({ // getter 137 | name: "get_" + k, 138 | access: [APrivate, AInline], 139 | kind: FFun( { 140 | args: [], 141 | ret: item.ctype, 142 | expr: switch (item.mode) { 143 | case Elem if(item.markup.ctype != item.ctype): macro return cast $edom; 144 | case Elem: macro return $edom; 145 | case Attr: macro return $edom.getAttribute($v{ aname }); 146 | case Prop: macro return $edom.$aname; 147 | case Style: macro return $edom.style.$aname; // return nvd.Dt.getCss($edom, $v{aname})??? 148 | } 149 | }), 150 | pos: item.markup.pos, 151 | }); 152 | if (item.readOnly) 153 | continue; 154 | fields.push({ 155 | name: "set_" + k, 156 | access: [APrivate, AInline], 157 | kind: FFun({ 158 | args: [{name: "v", type: item.ctype}], 159 | ret: item.ctype, 160 | expr: switch (item.mode) { 161 | case Attr: macro { $edom.setAttribute($v{ aname }, v); return v; } 162 | case Prop: macro return $edom.$aname = v; 163 | case Style: macro return $edom.style.$aname = v; 164 | default: throw "ERROR"; 165 | } 166 | }), 167 | pos: item.markup.pos, 168 | }); 169 | } 170 | 171 | if (comp.offset == 0) {// from Nvd.build 172 | Context.registerModuleDependency(cls.module, comp.path); 173 | } 174 | return fields; 175 | } 176 | 177 | // [1,0,3] => this.children[1].children[0].children[3] 178 | static function htmlChildren( a : Array, pos ) { 179 | var thiz = macro @:pos(pos) cast this; 180 | return a.length > 0 181 | ? Lambda.fold(a, (item, prev)-> macro @:pos(pos) (cast $prev).children[$v{item}], thiz) 182 | : thiz; 183 | } 184 | } 185 | #else 186 | extern class Macros{} 187 | #end -------------------------------------------------------------------------------- /src/nvd/inner/XMLComponent.hx: -------------------------------------------------------------------------------- 1 | package nvd.inner; 2 | 3 | import csss.xml.Xml; 4 | import haxe.macro.Expr; 5 | import haxe.macro.Context; 6 | import haxe.macro.PositionTools; 7 | 8 | using nvd.inner.Utils; 9 | import nvd.inner.HXX; 10 | 11 | class XMLComponent { 12 | 13 | var template : HXX; 14 | 15 | public var offset(default, null) : Int; 16 | 17 | public var path(default, null) : String; 18 | 19 | public var top(default, null) : Null; 20 | 21 | public var isSVG(default, null) : Bool; 22 | 23 | public var selector : Null; 24 | 25 | public function new( path, offset, node, selector, svg : Bool, useHXX : Bool ) { 26 | top = node; 27 | isSVG = svg; 28 | this.path = path; 29 | this.offset = offset; 30 | this.selector = selector; 31 | template = new HXX(useHXX); 32 | } 33 | 34 | public function topComplexType() : ComplexType { 35 | return Tags.ctype(top.nodeName, isSVG, false); 36 | } 37 | 38 | public function position( i : Int, len : Int ) : Position { 39 | return PositionTools.make({file: path, min: offset + i, max: offset + i + len}); 40 | } 41 | 42 | public inline function getChildPosition( child : Xml ) : Position { 43 | return position(child.nodePos, child.nodeName.length); 44 | } 45 | 46 | public function getChildPath( child : Xml ) : Array { 47 | var ret = []; 48 | while (child != top && child.parent != null) { 49 | var i = 0; 50 | var index = 0; 51 | var found = false; 52 | var siblings = @:privateAccess child.parent.children; 53 | while (i < siblings.length) { 54 | final elem = siblings[i]; 55 | if (elem.nodeType == Element) { 56 | if (elem == child) { 57 | found = true; 58 | break; 59 | } 60 | index++; 61 | } else if (elem.nodeType != PCData) { 62 | Nvd.fatalError("Comment/CDATA/ProcessingInstruction are not allowed here", getChildPosition(elem)); 63 | } 64 | i++; 65 | } 66 | if (!found) 67 | break; 68 | ret.push(index); 69 | child = child.parent; 70 | } 71 | if (child == top) { 72 | ret.reverse(); 73 | } else { 74 | ret = null; 75 | } 76 | return ret; 77 | } 78 | 79 | public function parse() : Expr { 80 | return parseInner(this.top, true); 81 | } 82 | 83 | function parseInner( xml : Xml, isTop : Bool ) : Expr { 84 | // attributes 85 | var attr = new Array(); 86 | var a = @:privateAccess xml.attributeMap; // [(attr, value), ...] 87 | var i = 0; 88 | while (i < a.length) { 89 | var name = a[i]; 90 | var value = a[i + 1]; 91 | var pos = this.position(xml.attrPos(name), name.length); 92 | attr.push( {field: name, expr: template.parse(value, pos)} ); 93 | i += 2; 94 | } 95 | var pos = this.getChildPosition(xml); 96 | // innerHTML 97 | var html = []; 98 | inline function PUSH(e) if (e != null) html.push(e); 99 | var children = @:privateAccess xml.children; 100 | for (child in children) { 101 | if (child.nodeType == Element) { 102 | PUSH( parseInner(child, false) ); 103 | } else if (child.nodeType == PCData) { 104 | var text = child.nodeValue; 105 | if (text == "") 106 | continue; 107 | PUSH( template.parse(text, this.position(child.nodePos, text.length)) ); 108 | } else { 109 | Nvd.fatalError("Comment/CDATA/ProcessingInstruction are not allowed here", pos); 110 | } 111 | } 112 | var ctype = topComplexType(); 113 | var name = xml.nodeName; 114 | var args = [macro $v{ name }]; 115 | if ( attr.length > 0 ) { 116 | args.push( {expr: EObjectDecl(attr), pos: pos} ); 117 | } else if (html.length > 0) { 118 | args.push( macro null ); 119 | } 120 | switch(html.length) { 121 | case 0: 122 | case 1: 123 | var one = html[0]; 124 | // hacks style.textContent for IE 125 | if (children[0].nodeType == PCData && name.toUpperCase() == "STYLE") { 126 | var ret = macro { 127 | final _css : nvd.Dt.VarString = $one; 128 | final _style = (cast nvd.Dt.Docs.createElement($e{ args[0] }) : $ctype); 129 | $b{ expandAttributes(macro _style, attr) }; 130 | if ((_style : Dynamic).styleSheet) { 131 | (_style : Dynamic).styleSheet.cssText = _css; 132 | } else if ((_style : Dynamic).textContent) { 133 | _style.textContent = _css; 134 | } else { 135 | _style.innerText = _css; 136 | } 137 | _style; 138 | } 139 | return ret; 140 | } 141 | args.push(one); 142 | default: 143 | args.push(macro $a{html}); 144 | } 145 | if (args.length == 1) 146 | return macro @:pos(pos) (cast nvd.Dt.Docs.createElement($e{ args[0] }) : $ctype); 147 | var ret = macro @:pos(pos) (cast nvd.Dt.h( $a{args} ) : $ctype); 148 | return isTop ? tryExpand(ret, args, pos, ctype) : ret; 149 | } 150 | 151 | function expandAttributes( node : Expr, attributes : Array ) : Array { 152 | var ret = []; 153 | for (attr in attributes) { 154 | var k = attr.field; 155 | var s = attr.expr; 156 | var e = switch(k) { 157 | case "id", "value", "name", "type", "src", "href", "title": 158 | macro $node.$k = $s; 159 | case "class": 160 | macro $node.className = $s; 161 | case "style": 162 | macro $node.style.cssText = $s; 163 | default: 164 | macro $node.setAttribute($v{ k }, $s); 165 | } 166 | e.pos = attr.expr.pos; 167 | ret.push(e); 168 | } 169 | return ret; 170 | } 171 | 172 | function tryExpand( origin : Expr, args : Array, pos : Position, ctype : ComplexType ) : Expr { 173 | var attr = args[1]; 174 | var content = args[2]; 175 | var mode = HXX.WhatMode.detects(content); 176 | if (mode == TCComplex) 177 | return origin; 178 | var battr = switch(attr.expr) { 179 | case EObjectDecl(a): 180 | expandAttributes(macro node, a); 181 | default: 182 | []; 183 | } 184 | var content = switch(mode) { 185 | case TCString: 186 | macro node.innerText = $content; 187 | case TCNode: 188 | macro node.appendChild($content); 189 | default: // TCNull 190 | macro {}; 191 | } 192 | return macro @:pos(pos) { 193 | final node : $ctype = cast nvd.Dt.Docs.createElement($e{ args[0] }); 194 | $b{ battr }; 195 | $content; 196 | node; 197 | }; 198 | } 199 | 200 | public static function fromMarkup( markup : Expr, isHXX : Bool ) : XMLComponent { 201 | var pos = PositionTools.getInfos(markup.pos); 202 | var top = markup.doParse().firstElement(); 203 | return new XMLComponent(pos.file, pos.min, top, null, top.isSVG(), isHXX); 204 | } 205 | } 206 | 207 | -------------------------------------------------------------------------------- /src/nvd/inner/AObject.hx: -------------------------------------------------------------------------------- 1 | package nvd.inner; 2 | 3 | using haxe.macro.Tools; 4 | using haxe.macro.PositionTools; 5 | import haxe.macro.Expr; 6 | import haxe.macro.Type; 7 | import haxe.macro.Context; 8 | 9 | using nvd.inner.Utils; 10 | import nvd.inner.Utils.pfield; 11 | import nvd.inner.AObject.raise; 12 | import nvd.inner.Tags; 13 | 14 | using csss.Query; 15 | import csss.xml.Xml; 16 | import csss.xml.Parser; 17 | 18 | class Markup { 19 | public var ctype : ComplexType; // ComplexType by xml.tagName. If unrecognized then default is `:js.html.DOMElement` 20 | public var path : Array; // relative to TOP 21 | public var css : Null; // css-selector 22 | public var pos : Position; // css-selector position 23 | public var xml : Xml; // the XML Node 24 | var ownerField : FieldInfo; 25 | 26 | public function new( field, selector ) { 27 | ownerField = field; 28 | load(selector); 29 | } 30 | 31 | function load( selector : Expr ) { 32 | var component = ownerField.ownerAObject.component; 33 | switch (selector.expr) { 34 | case EConst(CIdent("null")) | EConst(CString("")): 35 | this.xml = component.top; 36 | this.path = []; 37 | case EConst(CString(s)): 38 | var xml = try component.top.querySelector(s) catch (_) null; 39 | if (xml == null) 40 | raise('Could not find "$s" in ${component.top.toSimpleString()}', selector.pos); 41 | this.css = s; 42 | this.xml = xml; 43 | this.path = component.getChildPath(xml); 44 | default: 45 | raise("UnSupported: " + selector.toString(), selector.pos); 46 | } 47 | this.pos = selector.pos; 48 | this.ctype = Tags.ctype(this.xml.nodeName, component.isSVG, true); 49 | } 50 | 51 | public function unify( type : Type ) { 52 | switch(type) { 53 | case TAbstract(t, params): 54 | var ac = t.get(); 55 | for (meta in ac.meta.get()) { 56 | if (meta.name != ":build") 57 | continue; 58 | var top : Xml = null; 59 | switch(meta.params[0].expr) { 60 | case ECall(macro Nvd.build, args) if (args.length >= 2): 61 | var path = args[0]; 62 | var css = args[1]; 63 | var cache = CachedXML.get(path); 64 | top = cache.xml.querySelector(css.getValue()); 65 | case ECall(macro Nvd.buildString, [markup, _]): 66 | top = markup.doParse().firstElement(); 67 | default: 68 | } 69 | if (top == this.xml || top.simplyCompare(this.xml)) 70 | return true; 71 | } 72 | default: 73 | } 74 | return Context.unify(this.ctype.toType(), type); 75 | } 76 | } 77 | 78 | enum FieldMode { 79 | Elem; 80 | Attr; 81 | Prop; 82 | Style; 83 | } 84 | 85 | class FieldInfo { // Field 86 | 87 | public var readOnly : Bool; // for "name" 88 | public var keepCSS : Bool; // keep css in output/runtime 89 | public var markup : Markup; // Associated makrup 90 | public var ctype : ComplexType; // the ctype of the attribute/property/style property name 91 | public var mode : FieldMode; // what mode of "name" 92 | public var name : String; // the attribute/property/style-property name 93 | 94 | @:allow(nvd.inner.Markup) 95 | var ownerAObject : AObject; 96 | var paths : Array<{name : String, pos : Position}>; // .style.display => ["display", "style"]; 97 | 98 | public function new( aobj : AObject, expr : Expr ) { 99 | paths = []; 100 | ownerAObject = aobj; 101 | keepCSS = false; 102 | loadrec(expr); 103 | } 104 | 105 | function loadrec( e : Expr ) { 106 | switch(e.expr) { 107 | case EField(e2, field): 108 | paths.push({name : field, pos : pfield(e.pos, e2.pos)}); 109 | loadrec(e2); 110 | case ECall(macro $i{"$"}, params): 111 | readPaths(); 112 | this.markup = new Markup(this, params[0]); 113 | this.compatible(params); 114 | this.readAccess(); 115 | case EArray(e, {expr : EConst(CString(name)), pos : pos}): 116 | paths.push({name : name, pos : pos}); 117 | loadrec(e); 118 | case ECheckType(e2, ct): 119 | loadrec(e2); // load info first 120 | if (!this.markup.unify(Context.resolveType(ct, e.pos))) 121 | raise(ct.toString() + " should be " + this.markup.ctype.toString(), e.pos); 122 | this.ctype = ct; 123 | case EParenthesis(e): 124 | loadrec(e); 125 | case EMeta(m, e): 126 | this.keepCSS = m.name == ":keep"; 127 | loadrec(e); 128 | default: 129 | raise("UnSupported: " + e.toString(), e.pos); 130 | } 131 | } 132 | 133 | function compatible( params : Array ) { 134 | var i = 1; 135 | var len = params.length; 136 | while (i < len) { 137 | var arg = params[i++]; 138 | // old keepCSS syntax 139 | switch(arg.expr) { 140 | case EConst(CIdent("true")): 141 | this.keepCSS = true; 142 | continue; 143 | case EConst(CIdent("false")): 144 | default: 145 | raise("UnExpected: " + arg.toString(), arg.pos); 146 | } 147 | } 148 | } 149 | 150 | @:access(nvd.inner.Tags) 151 | function readAccess(){ 152 | inline function posname() return this.paths[0].pos; // see readPaths() 153 | var access : FCTAccess = null; 154 | switch(this.mode) { 155 | case Elem: 156 | access = {ctype: this.markup.ctype, vacc: AccNo}; 157 | case Attr: 158 | access = {ctype: macro :String, vacc: AccNormal}; 159 | case Prop: 160 | access = Tags.access(markup.xml.nodeName, this.name, ownerAObject.component.isSVG); 161 | if (access == null) 162 | raise('<${markup.xml.nodeName}/> has no field "${this.name}"', posname()); 163 | if (!simpleValid(this.markup.xml, this.name)) 164 | access = {ctype: access.ctype, vacc: AccNo}; 165 | case Style: 166 | access = Tags.style_access.get(this.name); 167 | if (access == null) 168 | raise('Style has no field "${this.name}"', posname()); 169 | } 170 | this.ctype = access.ctype; 171 | this.readOnly = access.vacc != AccNormal; 172 | } 173 | 174 | // $("css").style.display => ["display", "style"] 175 | function readPaths() { 176 | var len = paths.length; 177 | switch(len) { 178 | case 0: 179 | this.mode = Elem; 180 | case 1: 181 | this.mode = Prop; 182 | this.name = paths[0].name; 183 | case 2: 184 | var mode = paths[1]; 185 | switch(mode.name) { 186 | case "attr": 187 | this.mode = Attr; 188 | case "style": 189 | this.mode = Style; 190 | case unknown: 191 | // e.g: "$('s').parentNode.firstChild" is Not Yet Supported. 192 | raise("UnSupported Mode: " + unknown, mode.pos); 193 | } 194 | this.name = paths[0].name; 195 | default: 196 | paths.resize(len - 2); 197 | paths.reverse(); 198 | var p1 = paths[0].pos; 199 | var p2 = paths[len - (2 + 1)].pos; 200 | raise("Not Yet Supported: " + paths.map(o -> o.name).join("."), p1.punion(p2)); 201 | } 202 | } 203 | 204 | @:access(csss.xml.Xml) 205 | static function simpleValid( xml : Xml, prop : String ): Bool { 206 | return switch (prop) { 207 | case "textContent", "innerText": 208 | xml.children.length == 1 && xml.firstChild().nodeType == PCData; 209 | default: 210 | true; 211 | } 212 | } 213 | } 214 | 215 | class AObjectError { 216 | public var msg : String; 217 | public var pos : Position; 218 | public function new( msg, pos ) { 219 | this.msg = msg; 220 | this.pos = pos; 221 | } 222 | } 223 | 224 | /** 225 | * parsed data from enum abstract. 226 | */ 227 | class AObject { 228 | 229 | public var bindings(default, null) : Map; 230 | 231 | public var component(default, null) : XMLComponent; 232 | 233 | public function new(comp) { 234 | component = comp; 235 | bindings = new Map(); 236 | } 237 | 238 | public function parse( defs : Expr ) { 239 | switch (defs.expr) { 240 | case EBlock([]), EConst(CIdent("null")): // if null or {} then skip it 241 | case EObjectDecl(a): 242 | for (f in a) { 243 | if ( bindings.exists(f.field) ) 244 | raise("Duplicate definition", f.expr.pos); 245 | bindings.set(f.field, new FieldInfo(this, f.expr)); 246 | } 247 | default: 248 | raise("UnSupported: " + '"defs"', defs.pos); 249 | } 250 | } 251 | 252 | public static function raise( msg : String, pos : Position ) : Dynamic throw new AObjectError(msg, pos); 253 | } 254 | -------------------------------------------------------------------------------- /src/nvd/inner/Tags.hx: -------------------------------------------------------------------------------- 1 | package nvd.inner; 2 | 3 | import haxe.macro.Expr; 4 | import haxe.macro.Type; 5 | import haxe.macro.Context; 6 | using haxe.macro.Tools; 7 | using StringTools; 8 | 9 | @:structInit 10 | class FCTAccess { 11 | public var ctype(default, null) : ComplexType; 12 | public var vacc(default, null) : VarAccess; 13 | public function new(ctype, vacc) { 14 | this.ctype = ctype; 15 | this.vacc = vacc; 16 | } 17 | } 18 | 19 | class Tags { 20 | 21 | @:persistent static var ct_maps : Map; // tagname => complexType 22 | @:persistent static var dom_field_access : Map; // default field name => FCTAccess 23 | @:persistent static var style_access : Map; // css name => FCTAccess 24 | 25 | @:persistent static var htmls : haxe.DynamicAccess; // html tagname => moudle name 26 | @:persistent static var svgs : haxe.DynamicAccess; // svg tagname => moudle name 27 | @:persistent static var html_access_pool : Map>; // html tagName => [dom_field_access] 28 | @:persistent static var svg_access_pool : Map>; // svg tagName => [dom_field_access] 29 | 30 | public static function access( tagName : String, property : String, isSVG : Bool ) : FCTAccess { 31 | var map = isSVG ? svg_access_pool.get(tagName) : html_access_pool.get(tagName.toUpperCase()); 32 | if (map != null) { 33 | var ret = map.get(property); 34 | if (ret != null) 35 | return ret; 36 | } 37 | return dom_field_access.get(property); 38 | } 39 | 40 | public static function ctype( name : String, svg : Bool, access : Bool ) : ComplexType { 41 | if (!svg) name = name.toUpperCase(); 42 | var mod = toModule(name, svg); 43 | var ct = ct_maps.get(mod); 44 | if (ct == null) { 45 | var type = try Context.getType(mod) catch(e) Context.getType("js.html.DOMElement"); 46 | if (access) { 47 | var pool = svg ? svg_access_pool : html_access_pool; 48 | var fc = pool.get(name); 49 | if (fc == null) { 50 | fc = new Map(); 51 | loadFVar(fc, type); 52 | pool.set(name, fc); 53 | } 54 | } 55 | ct = type.toComplexType(); 56 | ct_maps.set(mod, ct); 57 | } 58 | return ct; 59 | } 60 | 61 | public static function toModule( name : String, isSVG : Bool ) : String { 62 | if (isSVG) { 63 | var type = svgs.get(name); // keep the original case 64 | if (type == null) { 65 | if (name.startsWith("fe")) { 66 | type = "FE" + name.substr(2) + "Element"; 67 | } else { 68 | type = name.charAt(0).toUpperCase() + name.substr(1) + "Element"; 69 | } 70 | svgs.set(name, type); 71 | } 72 | return "js.html." + "svg." + type; 73 | } else { 74 | var type = htmls.get(name); 75 | if (type == null) { 76 | type = name.charAt(0) + name.substr(1).toLowerCase() + "Element"; 77 | htmls.set(name, type); 78 | } 79 | return "js.html." + type; 80 | } 81 | } 82 | 83 | static function loadFVar( out : Map, type : Type, stop = "js.html.Element" ) : Void { 84 | switch (type) { 85 | case TInst(r, _): 86 | var c: ClassType = r.get(); 87 | while (true) { 88 | if (c.module == stop || c.module.substr(0, 7) != "js.html") 89 | break; 90 | var fs = c.fields.get(); 91 | for (f in fs) { 92 | switch (f.kind) { 93 | case FVar(_, w): 94 | out.set(f.name, { ctype: typect(f.type), vacc: w }); 95 | default: 96 | } 97 | } 98 | if (stop == "" || c.superClass == null) 99 | break; 100 | c = c.superClass.t.get(); 101 | } 102 | default: 103 | Nvd.fatalError("Unsupported type", (macro {}).pos); 104 | } 105 | } 106 | 107 | static function typect( t : Type ) : ComplexType { 108 | var name = switch (t) { 109 | case TInst(r, _): 110 | r.toString(); 111 | case TAbstract(r, _): 112 | r.toString(); // ??do follow(Abstract) 113 | default: 114 | null; 115 | } 116 | var ct: ComplexType = name == null ? null : ct_maps.get(name); 117 | if (ct == null) { 118 | ct = t.toComplexType(); 119 | if (name != null) 120 | ct_maps.set(name, ct); 121 | } 122 | return ct; 123 | } 124 | 125 | static function __init__() { 126 | init(); 127 | } 128 | 129 | static function init() { 130 | if (html_access_pool != null) return; 131 | 132 | ct_maps = new Map(); 133 | html_access_pool = new Map(); 134 | svg_access_pool = new Map(); 135 | 136 | dom_field_access = new Map(); 137 | loadFVar(dom_field_access, Context.getType("js.html.DOMElement"), "js.html.EventTarget"); 138 | 139 | style_access = new Map(); 140 | loadFVar(style_access, Context.getType("js.html.CSSStyleDeclaration"), ""); 141 | 142 | // All commented items could be (tagName + "Element") 143 | htmls = { 144 | "A" : "AnchorElement", 145 | // "AREA" : "AreaElement", 146 | // "AUDIO" : "AudioElement", 147 | // "BASE" : "BaseElement", 148 | // "BODY" : "BodyElement", 149 | "BR" : "BRElement", 150 | // "BUTTON" : "ButtonElement", 151 | // "CANVAS" : "CanvasElement", 152 | // "DATA" : "DataElement", 153 | "DATALIST" : "DataListElement", 154 | // "DIV" : "DivElement", 155 | // "EMBED" : "EmbedElement", 156 | "FIELDSET" : "FieldSetElement", 157 | // "FONT" : "FontElement", 158 | // "FORM" : "FormElement", 159 | // "FRAME" : "FrameElement", 160 | "FRAMESET" : "FrameSetElement", 161 | // "HEAD" : "HeadElement", 162 | "H1" : "HeadingElement", 163 | "H2" : "HeadingElement", 164 | "H3" : "HeadingElement", 165 | "H4" : "HeadingElement", 166 | "H5" : "HeadingElement", 167 | "H6" : "HeadingElement", 168 | "HR" : "HRElement", 169 | // "HTML" : "HtmlElement", 170 | // "IFRAME" : "IFrameElement", 171 | "IMG" : "ImageElement", 172 | // "INPUT" : "InputElement", 173 | // "LABEL" : "LabelElement", 174 | // "LEGEND" : "LegendElement", 175 | "LI" : "LIElement", 176 | // "LINK" : "LinkElement", 177 | 178 | // "MENU" : "MenuElement", 179 | "MENUITEM" : "MenuItemElement", 180 | // "META" : "MetaElement", 181 | // "METER" : "MeterElement", 182 | 183 | "INS" : "ModElement", 184 | "DEL" : "ModElement", 185 | // "OBJECT" : "ObjectElement", 186 | "OL" : "OListElement", 187 | "OPTGROUP" : "OptGroupElement", 188 | // "OPTION" : "OptionElement", 189 | // "OUTPUT" : "OutputElement", 190 | "P" : "ParagraphElement", 191 | // "PARAM" : "ParamElement", 192 | // "PRE" : "PreElement", 193 | "BLOCKQUOTE" : "QuoteElement", 194 | "Q" : "QuoteElement", 195 | // "SCRIPT" : "ScriptElement", 196 | // "SELECT" : "SelectElement", 197 | // "SOURCE" : "SourceElement", 198 | // "SPAN" : "SpanElement", 199 | // "STYLE" : "StyleElement", 200 | "CAPTION" : "TableCaptionElement", 201 | "TH" : "TableCellElement", 202 | "TD" : "TableCellElement", 203 | "COL" : "TableColElement", 204 | "COLGROUP" : "TableColElement", 205 | // "TABLE" : "TableElement", 206 | "TR" : "TableRowElement", 207 | "THEAD" : "TableSectionElement", 208 | "TBODY" : "TableSectionElement", 209 | "TFOOT" : "TableSectionElement", 210 | // "TEMPLATE" : "TemplateElement", 211 | "TEXTAREA" : "TextAreaElement", 212 | // "TITLE" : "TitleElement", 213 | // "TRACK" : "TrackElement", 214 | "UL" : "UListElement", 215 | // "VIDEO" : "VideoElement", 216 | } 217 | 218 | svgs = { 219 | //"a" : "AElement", 220 | //"animate" : "AnimateElement", 221 | //"animateMotion" : "AnimateMotionElement", 222 | //"animateTransform" : "AnimateTransformElement", 223 | //"circle" : "CircleElement", 224 | //"clipPath" : "ClipPathElement", 225 | //"defs" : "DefsElement", 226 | //"desc" : "DescElement", 227 | //"ellipse" : "EllipseElement", 228 | //"feBlend" : "FEBlendElement", 229 | //"feColorMatrix" : "FEColorMatrixElement", 230 | //"feComponentTransfer" : "FEComponentTransferElement", 231 | //"feComposite" : "FECompositeElement", 232 | //"feConvolveMatrix" : "FEConvolveMatrixElement", 233 | //"feDiffuseLighting" : "FEDiffuseLightingElement", 234 | //"feDisplacementMap" : "FEDisplacementMapElement", 235 | //"feDistantLight" : "FEDistantLightElement", 236 | //"feDropShadow" : "FEDropShadowElement", 237 | //"feFlood" : "FEFloodElement", 238 | //"feFuncA" : "FEFuncAElement", 239 | //"feFuncB" : "FEFuncBElement", 240 | //"feFuncG" : "FEFuncGElement", 241 | //"feFuncR" : "FEFuncRElement", 242 | //"feGaussianBlur" : "FEGaussianBlurElement", 243 | //"feImage" : "FEImageElement", 244 | //"feMerge" : "FEMergeElement", 245 | //"feMergeNode" : "FEMergeNodeElement", 246 | //"feMorphology" : "FEMorphologyElement", 247 | //"feOffset" : "FEOffsetElement", 248 | //"fePointLight" : "FEPointLightElement", 249 | //"feSpotLight" : "FESpotLightElement", 250 | //"feTile" : "FETileElement", 251 | //"feTurbulence" : "FETurbulenceElement", 252 | //"filter" : "FilterElement", 253 | //"foreignObject" : "ForeignObjectElement", 254 | //"g" : "GElement", 255 | //"image" : "ImageElement", 256 | //"linearGradient" : "LinearGradientElement", 257 | //"line" : "LineElement", 258 | //"marker" : "MarkerElement", 259 | //"mask" : "MaskElement", 260 | //"metadata" : "MetadataElement", 261 | //"mpath" : "MPathElement", 262 | //"path" : "PathElement", 263 | //"pattern" : "PatternElement", 264 | //"polygon" : "PolygonElement", 265 | //"polyline" : "PolylineElement", 266 | //"radialGradient" : "RadialGradientElement", 267 | //"rect" : "RectElement", 268 | //"script" : "ScriptElement", 269 | //"set" : "SetElement", 270 | //"stop" : "StopElement", 271 | //"style" : "StyleElement", 272 | "svg" : "SVGElement", 273 | //"switch" : "SwitchElement", 274 | //"symbol" : "SymbolElement", 275 | //"text" : "TextElement", 276 | //"textPath" : "TextPathElement", 277 | //"title" : "TitleElement", 278 | "tspan" : "TSpanElement", 279 | //"use" : "UseElement", 280 | //"view" : "ViewElement", 281 | } 282 | } 283 | } --------------------------------------------------------------------------------