Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use tink_hxx for parsing #95

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions haxelib.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@
"classPath": "src/lib",
"dependencies":
{
"tink_hxx": "0.15.2"
}
}
2 changes: 2 additions & 0 deletions src/lib/react/ReactDebugMacro.hx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package react;

#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.Type;
import haxe.macro.TypeTools;
#end

class ReactDebugMacro
{
Expand Down
254 changes: 161 additions & 93 deletions src/lib/react/ReactMacro.hx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package react;

import react.jsx.JsxParser;
import react.jsx.JsxSanitize;

#if macro
import react.jsx.HtmlEntities;
import tink.hxx.Parser;
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.ExprTools;
import haxe.macro.Type;
import haxe.ds.Option;
import tink.hxx.Node;
import tink.hxx.StringAt;
using tink.MacroApi;
import react.jsx.JsxStaticMacro;

#if (haxe_ver < 4)
Expand All @@ -18,6 +20,16 @@ typedef ComponentInfo = {
isExtern:Bool,
props:Array<ObjectField>
}

private class JsxParser extends tink.hxx.Parser {
public function new(source) {
super(source, JsxParser.new, { fragment: 'react.Fragment', defaultExtension: 'html' });
}
override function tagName() {
allow("$");
return super.tagName();
}
}
#end

/**
Expand All @@ -27,95 +39,149 @@ class ReactMacro
{
public static macro function jsx(expr:ExprOf<String>):Expr
{
if (Context.defined('display'))
return macro untyped ${expr};
else
return parseJsx(ExprTools.getValue(expr), expr.pos);
return switch tink.hxx.Parser.parseRootWith(expr, JsxParser.new).value {
case [v]: child(v);
case []: expr.reject('empty jsx');
default: expr.reject('only one node allowed here');
};
}

public static macro function sanitize(expr:ExprOf<String>):Expr
{
return macro $v{JsxSanitize.process(ExprTools.getValue(expr))};
}

/* PARSER */

#if macro
static var componentsMap:Map<String, ComponentInfo> = new Map();

static function parseJsx(jsx:String, pos:Position):Expr
static public function replaceEntities(value:String, pos:Position)
{
jsx = JsxSanitize.process(jsx);
var xml =
try
Xml.parse(jsx)
#if (haxe_ver >= 3.3)
catch(err:haxe.xml.Parser.XmlParserException)
{
var posInfos = Context.getPosInfos(pos);
var realPos = Context.makePosition({
file: posInfos.file,
min: posInfos.min + err.position,
max: posInfos.max + err.position,
if (value.indexOf('&') < 0)
return value;

var reEntity = ~/&[a-z0-9]+;/gi,
result = '',
index = 0;

while (reEntity.matchSub(value, index))
{
result += reEntity.matchedLeft();
var entity = reEntity.matched(0);
index = result.length + entity.length;

result += switch HtmlEntities.map[entity] {
case null:
var infos = Context.getPosInfos(pos);
infos.max = infos.min + index;
infos.min = infos.min + index - entity.length;
Context.makePosition(infos).warning('unknown entity $entity');
entity;
case e: e;
}

}

result += value.substr(index);
//TODO: consider giving warnings for isolated `&`
return result;
}
static function children(c:tink.hxx.Children) {
var exprs = switch c {
case null | { value: null }: [];
default: [for (c in tink.hxx.Generator.normalize(c.value)) child(c)];
}
return {
individual: exprs,
compound: switch exprs {
case []: null;
case [v]: v;
case a: macro @:pos(c.pos) ($a{a}:Array<Dynamic>);
}
}
}

static function typeChecker(type:Expr, isHtml:Bool) {
function propsFor(placeholder:Expr):StringAt->Expr->Void {
placeholder = Context.storeTypedExpr(Context.typeExpr(placeholder));
return function (name:StringAt, value:Expr) {
var field = name.value;
var target = macro @:pos(name.pos) $placeholder.$field;
Context.typeof(macro @:pos(value.pos) {
var __pseudo = $target;
__pseudo = $value;
});
Context.fatalError('Invalid JSX: ' + err.message, realPos);
}
#end
catch(err:Dynamic)
Context.fatalError('Invalid JSX: ' + err, err.pos ? err.pos : pos);

var ast = JsxParser.process(xml);
var expr = parseJsxNode(ast, pos);
return expr;
}
return
if (isHtml) function (_, _) {}
else switch type.typeof().sure() {
case TFun(args, _):

switch args {
case []: function (_, e:Expr) e.reject('no props allowed here');
case [v]: propsFor(macro @:pos(type.pos) {
var o = null;
$type(o);
o;
});
case v: throw 'assert';//TODO: do something meaningful here
}
default:
propsFor(macro @:pos(type.pos) {
function get<T>(c:Class<T>):T {
return null;
}
@:privateAccess get($type).props;
});
}
}

static function parseJsxNode(ast:JsxAst, pos:Position)
{
switch (ast)
{
case JsxAst.Text(value):
return macro $v{value};

case JsxAst.Expr(value):
return Context.parse(value, pos);

case JsxAst.Node(isHtml, path, attributes, jsxChildren):
// parse type
var type = isHtml ? macro $v{path[0]} : macro $p{path};
type.pos = pos;
static function child(c:Child)
return switch c.value {
case CText(s): macro @:pos(s.pos) $v{replaceEntities(s.value, s.pos)};
case CExpr(e): e;
case CNode(n):
var type =
switch n.name.value.split('.') {
case [tag] if (tag.charAt(0) == tag.charAt(0).toLowerCase()):
macro @:pos(n.name.pos) $v{tag};
case parts:
macro @:pos(n.name.pos) $p{parts};
}

// handle @:jsxStatic
var isHtml = type.getString().isSuccess();//TODO: this is a little awkward
if (!isHtml) JsxStaticMacro.handleJsxStaticProxy(type);

// parse attributes
var attrs = [];
var spread = [];
var key = null;
var ref = null;
for (attr in attributes)
{
var expr = parseJsxAttr(attr.value, pos);
var name = attr.name;
if (name == 'key') key = expr;
else if (name == 'ref') ref = expr;
else if (name.charAt(0) == '.') spread.push(expr);
else attrs.push({ field:name, expr:expr });

var checkProp = typeChecker(type, isHtml),
attrs = new Array<ObjectField>(),
spread = [],
key = null,
ref = null,
pos = n.name.pos;

function add(name:StringAt, e:Expr) {
checkProp(name, e);
attrs.push({ field: name.value, expr: e });
}

for (attr in n.attributes) switch attr {
case Splat(e): spread.push(e);
case Empty(invalid = { value: 'key' | 'ref'}): invalid.pos.error('attribute ${invalid.value} must have a value');
case Empty(name): add(name, macro @:pos(name.pos) true);
case Regular(name, value):
var expr = value.getString().map(function (s) return macro $v{replaceEntities(s, value.pos)}).orUse(value);
switch name.value {
case 'key': key = expr;
case 'ref': ref = expr;
default: add(name, value);
}
}
// parse children
var children = [for (child in jsxChildren) parseJsxNode(child, pos)];
var children = children(n.children);

// inline declaration or createElement?
var typeInfo = getComponentInfo(type);
JsxStaticMacro.injectDisplayNames(type);
var useLiteral = canUseLiteral(typeInfo, ref);

if (useLiteral)
{
if (children.length > 0)
if (children.compound != null)
{
// single child should not be placed in an Array
if (children.length == 1) attrs.push({field:'children', expr:macro ${children[0]}});
else attrs.push({field:'children', expr:macro ($a{children} :Array<Dynamic>)});
attrs.push({field:'children', expr: children.compound });
}
if (!isHtml)
{
Expand All @@ -127,7 +193,7 @@ class ReactMacro
}
}
var props = makeProps(spread, attrs, pos);
return genLiteral(type, props, ref, key, pos);
genLiteral(type, props, ref, key, pos);
}
else
{
Expand All @@ -136,42 +202,44 @@ class ReactMacro

var props = makeProps(spread, attrs, pos);

var args = [type, props].concat(children);
return macro react.React.createElement($a{args});
var args = [type, props].concat(children.individual);
macro @:pos(n.name.pos) react.React.createElement($a{args});
}
case CSplat(_):
c.pos.error('jsx does not support child splats');
case CIf(cond, cons, alt):
macro @:pos(cond.pos) if ($cond) ${body(cons)} else ${body(alt)};
case CFor(head, expr):
macro @:pos(head.pos) ([for ($head) ${body(expr)}]:Array<Dynamic>);
case CSwitch(target, cases):
ESwitch(target, [for (c in cases) {
guard: c.guard,
values: c.values,
expr: body(c.children)
}], null).at(target.pos);
default: c.pos.error('jsx does not support control structures');//already disabled at parser level anyway
}
}

static function parseJsxAttr(value:String, pos:Position)
{
var ast = JsxParser.parseText(value);
return switch (ast)
{
case JsxAst.Text(value):
return macro $v{value};

case JsxAst.Expr(value):
return Context.parse(value, pos);

default: null;
}
}
static function body(c:Children)
return macro ($a{children(c).individual}:Array<Dynamic>);

static var componentsMap:Map<String, ComponentInfo> = new Map();

static function genLiteral(type:Expr, props:Expr, ref:Expr, key:Expr, pos:Position)
{
if (key == null) key = macro null;
if (ref == null) ref = macro null;

var fields = [
{field: "@$__hx__$$typeof", expr: macro untyped __js__("$$tre")},
var fields:Array<ObjectField> = [
{field: #if (haxe_ver < 4) "@$__hx__$$typeof" #else "$$typeof", quotes: DoubleQuotes #end, expr: macro untyped __js__("$$tre")},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't it be quotes: Quoted instead?
HaxeFoundation/haxe@b6e5d66

{field: 'type', expr: type},
{field: 'props', expr: props}
];
if (key != null) fields.push({field: 'key', expr: key});
if (ref != null) fields.push({field: 'ref', expr: ref});
var obj = {expr: EObjectDecl(fields), pos: pos};

return macro ($obj : react.ReactComponent.ReactElement);
return macro @:pos(pos) ($obj : react.ReactComponent.ReactElement);
}

static function canUseLiteral(typeInfo:ComponentInfo, ref:Expr)
Expand Down
Loading