diff --git a/demo.hxml b/demo.hxml index a656384..3a501a4 100644 --- a/demo.hxml +++ b/demo.hxml @@ -4,3 +4,5 @@ -lib coconut.storybook -lib coconut.vdom + +-D analyzer-optimize \ No newline at end of file diff --git a/extraParams.hxml b/extraParams.hxml new file mode 100644 index 0000000..a5ad0c1 --- /dev/null +++ b/extraParams.hxml @@ -0,0 +1 @@ +--macro coconut.storybook.Setup.setup() \ No newline at end of file diff --git a/haxe_libraries/coconut.storybook.hxml b/haxe_libraries/coconut.storybook.hxml index 300b11f..bf9720e 100644 --- a/haxe_libraries/coconut.storybook.hxml +++ b/haxe_libraries/coconut.storybook.hxml @@ -1,4 +1,5 @@ -lib coconut.ui --cp ${SCOPE_DIR}/./src +-cp ${SCOPE_DIR}/../coconut.storybook/src -D coconut.storybook=0.0.0 +${SCOPE_DIR}/../coconut.storybook/extraParams.hxml --macro Sys.println("haxe_libraries/coconut.storybook.hxml:3: [Warning] Using dev version of library coconut.storybook") \ No newline at end of file diff --git a/src/coconut/storybook/Component.macro.hx b/src/coconut/storybook/Component.macro.hx index 4ba0f27..2f070bf 100644 --- a/src/coconut/storybook/Component.macro.hx +++ b/src/coconut/storybook/Component.macro.hx @@ -1,14 +1,20 @@ package coconut.storybook; import haxe.macro.Context; +import haxe.macro.Expr; using tink.MacroApi; class Component { public static function build() { var builder = new ClassBuilder(); + if (!builder.target.meta.has(':tink')) builder.target.meta.add(':tink', [], Context.currentPos()); + + if (!builder.target.meta.has(':storybook')) + builder.target.meta.add(':storybook', [], Context.currentPos()); + return null; } diff --git a/src/coconut/storybook/Setup.hx b/src/coconut/storybook/Setup.hx new file mode 100644 index 0000000..5996dee --- /dev/null +++ b/src/coconut/storybook/Setup.hx @@ -0,0 +1,103 @@ +package coconut.storybook; + +import haxe.macro.Context; +import haxe.macro.Expr; +import tink.SyntaxHub; + +using StringTools; +using tink.MacroApi; + +class Setup { + public static function setup() { + var debug = false; + SyntaxHub.classLevel.after('tink.lang.Sugar', builder -> { + if (!builder.target.meta.has(':tink')) + return false; + + for (field in builder) + switch [field.kind, field.metaNamed(':story'), field.metaNamed(':state')] { + case [FFun(func), stories, v] if (stories.length > 0): + // wrap with Isolated + function subst(e:Expr) + return switch e { + case macro return $ret: + macro return coconut.ui.Isolated.fromHxx({}, {children: $ret}); + case e: + e; + } + func.expr = func.expr.map(subst); + + // add states + for (states in v) + for (state in states.params) + switch state.expr { + case EVars(vars): + for (v in vars) { + if (v.expr == null) + state.pos.error('@:state var ${v.name} requires a initializer'); + if (v.type == null) + state.pos.error('@:state var ${v.name} requires a type hint'); + + var name = v.name; + var alias = getAlias(name); + var ct = v.type; + + var original = func.expr; + func.expr = macro { + var $name = new tink.state.State<$ct>(${v.expr}); + var $alias = $i{name}; + ${func.expr} + } + } + + case _: + state.pos.error('Only supports EVars expressions'); + } + + case _: + // skip + } + + return true; + }); + + SyntaxHub.exprLevel.inward.after(_ -> true, { + appliesTo: builder -> builder.target.meta.has(':storybook'), + apply: (e:Expr) -> { + function transform(name:String, original:Expr, transformed:Expr) { + return switch Context.getLocalTVars()[name] { + case null: + original; + case {t: _.getID() => 'tink.state.State'}: + transformed; + case _: + original; + } + } + + switch e.expr { + case EConst(CIdent(name)) if (!isAlias(name)): + transform(name, e, macro $i{getAlias(name)}.value); + + case EBinop(OpAssign, macro $i{name}, rhs) if (!isAlias(name)): + transform(name, e, macro $i{getAlias(name)}.set($rhs)); + + case EBinop(OpAssignOp(binop), macro $i{name}, rhs) if (!isAlias(name)): + var rhs = EBinop(binop, macro $i{getAlias(name)}.value, rhs).at(e.pos); + transform(name, e, macro $i{getAlias(name)}.set($rhs)); + + case _: + e; + } + } + }); + } + + inline static function getAlias(name:String) { + return '__alias_${name}'; + } + + inline static function isAlias(name:String) { + return name.startsWith('__alias_'); + } +} diff --git a/src/coconut/storybook/Storybook.macro.hx b/src/coconut/storybook/Storybook.macro.hx index de83fbd..8a5b541 100644 --- a/src/coconut/storybook/Storybook.macro.hx +++ b/src/coconut/storybook/Storybook.macro.hx @@ -7,6 +7,12 @@ using tink.MacroApi; class Storybook { public static macro function add(exprs:Array):Expr { + switch exprs { + case [{expr: EArrayDecl(values)}]: + exprs = values; + case _: + } + var ret = []; for (expr in exprs) { diff --git a/tests/Demo.hx b/tests/Demo.hx index 88b5777..9154f83 100644 --- a/tests/Demo.hx +++ b/tests/Demo.hx @@ -5,7 +5,12 @@ import coconut.ui.*; class Demo { static function main() { - Storybook.add(new Button(), new foo.Bar()); + Storybook.add([ + // @formatter:off + new Button(), + new foo.Bar(), + // @formatter:on + ]); } } @@ -31,6 +36,14 @@ class Button extends Component { '; + @:story + @:state(var value:Int = 0) + function withState() ' + + '; + function wrap(f:()->RenderResult) '
${f()}
'; @@ -43,3 +56,10 @@ extern class Knobs { static function boolean(name:String, value:Bool):Bool; static function text(name:String, value:String):String; } + +class Isolated extends View { + @:attribute var children:Children; + + function render() + '<>${...children}'; +}