1 module libwasm.dom; 2 3 import libwasm.types; 4 import memutils.ct; 5 import std.traits : hasMember, isAggregateType; 6 import std.traits : TemplateArgsOf, staticMap, isPointer, PointerTarget, getUDAs, EnumMembers, isInstanceOf, isBasicType, isIntegral; 7 import std.traits : getSymbolsByUDA, getUDAs; 8 import std.meta : Filter, AliasSeq, ApplyLeft, ApplyRight; 9 import libwasm.css; 10 import libwasm.node; 11 import libwasm.event; 12 import std.meta : staticIndexOf; 13 import libwasm.array; 14 import libwasm.rt.array; 15 16 @safe: 17 nothrow: 18 19 version (unittest) 20 { 21 import libwasm.sumtype; 22 import std.array : Appender; 23 import std.algorithm : remove, countUntil; 24 import std.array : insertInPlace; 25 26 class UnittestDomNode 27 { 28 alias Property = SumType!(string, int, bool, double); 29 alias Attribute = SumType!(string, int); 30 NodeType type; 31 Handle handle; 32 UnittestDomNode parent; 33 Property[string] properties; 34 Attribute[string] attributes; 35 string[] classes; 36 Appender!(UnittestDomNode[]) children; 37 this(NodeType type, Handle handle) nothrow 38 { 39 this.type = type; 40 this.handle = handle; 41 } 42 43 void setAttribute(T)(string name, T value) nothrow @trusted 44 { 45 attributes[name] = Attribute(value); 46 } 47 48 Attribute getAttribute(string name) nothrow 49 { 50 return attributes[name]; 51 } 52 53 void setProperty(T)(string name, T value) nothrow @trusted 54 { 55 properties[name] = Property(value); 56 } 57 58 Property getProperty(string name) nothrow 59 { 60 return properties[name]; 61 } 62 63 void toString(scope void delegate(const(char)[]) @safe sink) @trusted 64 { 65 import std.algorithm : each; 66 import std.format : formattedWrite, format; 67 import std.array : join; 68 import std.conv : to; 69 70 if (type != NodeType.root) 71 { 72 sink.formattedWrite("<%s", type); 73 // write attributes and properties 74 if (classes.length > 0) 75 { 76 sink.formattedWrite(" class=\"%s\"", classes.join(" ")); 77 } 78 foreach (kv; properties.byKeyValue()) 79 { 80 sink.formattedWrite(" %s=%s", kv.key, libwasm.sumtype.match!((string s) => format(`"%s"`, s), ( 81 i) => i.to!string)(kv.value)); 82 } 83 foreach (kv; attributes.byKeyValue()) 84 { 85 sink.formattedWrite(" %s=%s", kv.key, libwasm.sumtype.match!((string s) => format(`"%s"`, s), ( 86 int i) => i.to!string)(kv.value)); 87 } 88 sink(">"); 89 } 90 children.data.each!(c => c.toString(sink)); 91 // write children 92 if (type != NodeType.root) 93 sink.formattedWrite("</%s>", type); 94 } 95 } 96 97 Appender!(UnittestDomNode[]) unittest_dom_nodes; 98 private auto getNode(Handle node) 99 { 100 assert(node > 0); 101 return unittest_dom_nodes.data[node - 1]; 102 } 103 104 private auto assumeNoException(Block)(lazy Block block) 105 { 106 try 107 { 108 block(); 109 } 110 catch (Exception e) 111 { 112 assert(0, e.msg); 113 } 114 } 115 116 extern (C) 117 { 118 Handle createElement(NodeType type) 119 { 120 uint idx = cast(uint) unittest_dom_nodes.data.length; 121 unittest_dom_nodes.put(new UnittestDomNode(type, idx + 1)); 122 return idx + 1; 123 } 124 125 void addClass(Handle node, string className) 126 { 127 node.getNode().classes ~= className; 128 } 129 130 void setProperty(Handle node, string prop, string value) 131 { 132 node.getNode().setProperty(prop, value); 133 } 134 135 void removeChild(Handle childPtr) @trusted 136 { 137 import std.algorithm : remove; 138 import std.algorithm : countUntil; 139 140 auto child = childPtr.getNode(); 141 auto childIdx = child.parent.children.data.countUntil!(c => c is child); 142 assert(childIdx >= 0); 143 child.parent.children.data.remove(childIdx); 144 child.parent.children.shrinkTo(child.parent.children.data.length - 1).assumeNoException(); 145 } 146 147 void unmount(Handle childPtr) 148 { 149 removeChild(childPtr); 150 } 151 152 void appendChild(Handle parentPtr, Handle childPtr) @trusted 153 { 154 auto parent = parentPtr.getNode(); 155 auto child = childPtr.getNode(); 156 if (child.parent) 157 { 158 auto childIdx = child.parent.children.data.countUntil!(c => c is child); 159 if (childIdx > 0) 160 { 161 child.parent.children.data.remove(childIdx); 162 child.parent.children.shrinkTo(child.parent.children.data.length - 1).assumeNoException(); 163 } 164 } 165 child.parent = parent; 166 parent.children.put(child); 167 } 168 169 void insertBefore(Handle parentPtr, Handle childPtr, Handle siblingPtr) @trusted 170 { 171 auto parent = parentPtr.getNode(); 172 auto child = childPtr.getNode(); 173 auto sibling = siblingPtr.getNode(); 174 if (child.parent) 175 { 176 auto childIdx = child.parent.children.data.countUntil!(c => c is child); 177 if (childIdx > 0) 178 { 179 child.parent.children.data.remove(childIdx); 180 child.parent.children.shrinkTo(child.parent.children.data.length - 1).assumeNoException(); 181 } 182 } 183 child.parent = parent; 184 auto siblingIdx = parent.children.data.countUntil!(c => c is sibling); 185 assert(siblingIdx >= 0); 186 parent.children.put(child); 187 auto arr = parent.children.data; 188 189 foreach (i; 0 .. arr.length - siblingIdx) 190 { 191 if (arr.length - i > 1) 192 arr[arr.length - 1 - i] = arr[arr.length - 2 - i]; 193 } 194 arr[siblingIdx] = child; 195 } 196 197 void setAttribute(Handle node, string attr, string value) 198 { 199 node.getNode().setAttribute(attr, value); 200 } 201 202 void setAttributeInt(Handle node, string attr, int value) 203 { 204 node.getNode().setAttribute(attr, value); 205 } 206 207 void setPropertyBool(Handle node, string prop, bool value) 208 { 209 node.getNode().setProperty(prop, value); 210 } 211 212 void setPropertyInt(Handle node, string prop, int value) 213 { 214 node.getNode().setProperty(prop, value); 215 } 216 217 void setPropertyDouble(Handle node, string prop, double value) 218 { 219 node.getNode().setProperty(prop, value); 220 } 221 222 void innerText(Handle nodePtr, string text) 223 { 224 } 225 226 void removeClass(Handle node, string className) 227 { 228 import std.algorithm : remove; 229 230 auto n = node.getNode(); 231 n.classes = n.classes.remove!(i => i == className); 232 } 233 234 void changeClass(Handle node, string className, bool on) 235 { 236 import std.algorithm : any; 237 238 auto n = node.getNode(); 239 if (on && !n.classes.any!(c => c == className)) 240 n.classes ~= className; 241 else if (!on) 242 removeClass(node, className); 243 } 244 245 Optional!string getProperty(Handle node, string name) 246 { 247 auto str = node.getNode().getProperty(name).trustedGet!string; 248 return some(str); 249 } 250 251 Optional!int getPropertyInt(Handle node, string prop) 252 { 253 return some(node.getNode().getProperty(prop).trustedGet!int); 254 } 255 256 Optional!bool getPropertyBool(Handle node, string prop) 257 { 258 return some(node.getNode().getProperty(prop).trustedGet!bool); 259 } 260 261 Optional!double getPropertyDouble(Handle node, string prop) 262 { 263 return some(node.getNode().getProperty(prop).trustedGet!double); 264 } 265 266 void setSelectionRange(Handle node, uint start, uint end) 267 { 268 } 269 270 void addCss(string css) 271 { 272 } 273 } 274 } 275 else 276 { 277 private extern (C) 278 { 279 Handle createElement(NodeType type); 280 void addClass(Handle node, string className); 281 void setProperty(Handle node, string prop, string value); 282 void removeChild(Handle childPtr); 283 void unmount(Handle childPtr); 284 void appendChild(Handle parentPtr, Handle childPtr); 285 void insertBefore(Handle parentPtr, Handle childPtr, Handle sibling); 286 void setAttribute(Handle nodePtr, string attr, string value); 287 void setAttributeInt(Handle nodePtr, string attr, int value); 288 void setPropertyBool(Handle nodePtr, string attr, bool value); 289 void setPropertyInt(Handle nodePtr, string attr, int value); 290 void setPropertyDouble(Handle nodePtr, string attr, double value); 291 void innerText(Handle nodePtr, string text); 292 void removeClass(Handle node, string className); 293 void changeClass(Handle node, string className, bool on); 294 } 295 296 extern (C) 297 { 298 string getProperty(Handle node, string prop); 299 int getPropertyInt(Handle node, string prop); 300 bool getPropertyBool(Handle node, string prop); 301 void getPropertyDouble(Handle nodePtr, string prop); 302 void setSelectionRange(Handle node, uint start, uint end); 303 void addCss(string css); 304 } 305 } 306 307 import libwasm.bindings.Document : Document; 308 import libwasm.bindings.Window : Window; 309 310 @trusted ref auto undefined() 311 { 312 static __gshared u = Any(JsHandle(0)); 313 return u; 314 } 315 316 @trusted ref auto document() 317 { 318 static __gshared d = Document(JsHandle(1)); 319 return d; 320 } 321 322 @trusted ref auto window() 323 { 324 static __gshared w = Window(JsHandle(2)); 325 return w; 326 } 327 328 void unmount(T)(auto ref T t) if (is(T : SumType!(Types), Types...)) 329 { 330 t.match!(unmount); 331 } 332 333 void unmount(T)(auto ref T t) if (hasMember!(T, "node")) 334 { 335 unmount(t.node.node); 336 static if (hasMember!(T, "node")) 337 t.node.mounted = false; 338 t.propagateOnUnmount(); 339 } 340 341 auto removeChild(T)(T* t) if (is(T : SumType!(Types), Types...)) 342 { 343 unmount(*t); 344 } 345 346 auto removeChild(T)(auto ref T t) if (hasMember!(T, "node")) 347 { 348 removeChild(t.node.node); 349 static if (hasMember!(T, "node")) 350 t.node.mounted = false; 351 t.propagateOnUnmount(); 352 } 353 354 auto focus(T)(auto ref T t) if (hasMember!(T, "node")) 355 { 356 t.node.node.focus(); 357 } 358 359 auto renderBefore(T, Ts...)(Handle parent, return auto ref T t, Handle sibling, return auto ref Ts ts) 360 { 361 static if (is(T : SumType!Types, Types...)) 362 { 363 t.match!((scope ref i) => renderBefore(parent, i, sibling, ts)); 364 } 365 else 366 { 367 if (parent == invalidHandle) 368 return; 369 renderIntoNode(parent, t, ts); 370 static if (hasMember!(T, "node")) 371 { 372 parent.insertBefore(t.node.node, sibling); 373 t.node.mounted = true; 374 } 375 t.propagateOnMount(); 376 } 377 } 378 379 auto render(T, Ts...)(Handle parent, return auto ref T t, return auto ref Ts ts) @trusted 380 { 381 static if (is(T : SumType!Types, Types...)) 382 { 383 t.match!((scope ref i) => render(parent, i, ts)); 384 } 385 else 386 { 387 if (parent == invalidHandle) 388 return; 389 renderIntoNode(parent, t, ts); 390 static if (hasMember!(T, "node")) 391 { 392 if (!t.getNamedNode().mounted) 393 { 394 parent.appendChild(t.getNamedNode.node); 395 t.getNamedNode().mounted = true; 396 } 397 } 398 t.propagateOnMount(); 399 } 400 } 401 402 import std.traits : isFunction; 403 404 auto propagateOnMount(T)(auto ref T t) 405 { 406 static foreach (c; getChildren!T) 407 __traits(getMember, t, c).propagateOnMount(); 408 static if (hasMember!(T, "onMount") && isFunction!(T.onMount)) 409 t.onMount(); 410 } 411 412 auto propagateOnUnmount(T)(auto ref T t) 413 { 414 static foreach (c; getChildren!T) 415 __traits(getMember, t, c).propagateOnMount(); 416 static if (hasMember!(T, "onUnmount") && isFunction!(T.onUnmount)) 417 t.onUnmount(); 418 } 419 420 auto remount(string field, Parent)(auto ref Parent parent) 421 { 422 import std.traits : hasUDA; 423 import std.meta : AliasSeq; 424 425 alias fields = AliasSeq!(__traits(allMembers, Parent)); 426 alias idx = staticIndexOf!(field, fields); 427 static if (fields.length > idx + 1) 428 { 429 static foreach (child; fields[idx + 1 .. $]) 430 { 431 static if (hasUDA!(__traits(getMember, Parent, child), libwasm.types.child)) 432 { 433 if (__traits(getMember, parent, child).node.mounted) 434 return renderBefore(parent.node.node, __traits(getMember, parent, field), __traits(getMember, parent, child) 435 .node.node, parent); 436 } 437 } 438 } 439 return render(parent.node.node, __traits(getMember, parent, field), parent); 440 } 441 442 template isValue(alias t) 443 { 444 enum isValue = __traits(compiles, { enum p = t; }); 445 } 446 447 template createParameterTuple(Params...) 448 { 449 auto createParameterTuple(Ts...)(return auto ref Ts ts) 450 { 451 import std.traits : TemplateArgsOf, staticMap; 452 import libwasm.spa : Param; 453 454 template extractName(Arg) 455 { 456 enum extractName = Arg.Name; 457 } 458 459 static auto extractField(alias sym)(return auto ref Ts ts) @trusted 460 { 461 enum name = sym.stringof; 462 enum literal = isValue!(sym); 463 static if (isValue!(sym)) 464 { 465 static if (isBasicType!(typeof(sym))) 466 { 467 typeof(sym) val = sym; 468 return val; 469 } 470 else 471 { 472 __gshared static auto val = sym; 473 return &val; 474 } 475 } 476 else 477 { 478 alias ContainingType = AliasSeq!(__traits(parent, sym))[0]; 479 template isContainingType(ContainingType, T) 480 { 481 enum isContainingType = is(ContainingType == T); 482 } 483 484 enum index = indexOfPred!(ApplyLeft!(isContainingType, ContainingType), AliasSeq!Ts); 485 return &__traits(getMember, ts[index], name); 486 } 487 } 488 489 static auto extractFields(Args...)(return auto ref Ts ts) @safe 490 if (Args.length > 0) 491 { 492 static if (Args.length > 1) 493 return tuple(extractField!(TemplateArgsOf!(Args[0])[1])(ts), extractFields!(Args[1 .. $])( 494 ts).expand); 495 else 496 return tuple(extractField!(TemplateArgsOf!(Args[0])[1])(ts)); 497 } 498 499 alias ParamsTuple = staticMap!(TemplateArgsOf, Params); 500 alias Names = staticMap!(extractName, ParamsTuple); 501 auto Fields = cast() extractFields!(ParamsTuple)(ts); 502 return tuple!(Names)(Fields.expand); 503 } 504 } 505 506 @trusted 507 void setParamFromParent(string injected_name, string local_name, T, Ts...)( 508 return auto ref T t, return auto ref Ts ts) 509 { 510 import std.traits : PointerTarget, isPointer; 511 import std.meta : AliasSeq; 512 513 alias TargetType = typeof(getMember!(T, local_name)); 514 static if (isPointer!(TargetType)) 515 alias FieldType = PointerTarget!(TargetType); 516 else 517 alias FieldType = TargetType; 518 template matchesField(Parent) 519 { 520 static if (!hasMember!(Parent, injected_name)) 521 enum matchesField = false; 522 else 523 { 524 alias ItemTypeParent = AliasSeq!(__traits(parent, getMember!(Parent, injected_name)))[0]; 525 static if (is(T == ItemTypeParent)) 526 enum matchesField = false; 527 else 528 { 529 alias ItemType = typeof(getMember!(Parent, injected_name)); 530 enum matchesField = (is(ItemType == FieldType) || is(ItemType == FieldType*)); 531 } 532 } 533 } 534 535 enum index = indexOfPred!(matchesField, AliasSeq!Ts); 536 static if (index >= ts.length) 537 { 538 return; 539 } 540 else 541 { 542 alias SourceType = typeof(__traits(getMember, Ts[index], injected_name)); 543 static if (isPointer!TargetType) 544 { 545 static if (isPointer!SourceType) 546 __traits(getMember, t, local_name) = __traits(getMember, ts[index], injected_name); 547 else 548 { 549 static if (is(Ts[index] : Tuple!Fields, Fields...)) 550 { 551 static assert(!isBasicType!SourceType, "Cannot assign literal in param uda to a pointer, use member field or avoid use of pointer."); 552 } 553 __traits(getMember, t, local_name) = &__traits(getMember, ts[index], injected_name); 554 } 555 } 556 else static if (isPointer!SourceType) 557 __traits(getMember, t, local_name) = *__traits(getMember, ts[index], injected_name); 558 else 559 { 560 __traits(getMember, t, local_name) = __traits(getMember, ts[index], injected_name); 561 } 562 } 563 } 564 565 void setChildFromParent(string injected_name, string local_name, T, Ts...)( 566 return auto ref T t, return auto ref Ts ts) 567 { 568 template containsChild(Parent) 569 { 570 enum containsChild = hasMember!(Parent, injected_name); 571 } 572 573 enum index = indexOfPred!(containsChild, AliasSeq!Ts); 574 static if (index != -1) 575 { 576 alias ChildMemberType = typeof(__traits(getMember, T, local_name)); 577 alias Parent = Ts[index]; 578 alias ParentMemberType = typeof(__traits(getMember, Parent, injected_name)); 579 static assert(__traits(hasMember, ParentMemberType, "node"), __traits(identifier, ParentMemberType) ~ " doesn't have a member 'node'."); 580 alias ParentMemberNodeType = typeof(ParentMemberType.node); 581 alias ChildMemberNodeType = typeof(ChildMemberType.node); 582 static assert(is(ParentMemberNodeType : NamedNode!type, string type), __traits(identifier, ParentMemberType) ~ "'s member 'node' needs to be of type NamedNode."); 583 // NOTE: for now we assert that the ChildMemberNodeType is an HTMLElement, it would be nice to 584 // allow the component to restrict the nodetype of child elements, in which case 585 // we need to check if ChildMemberNodeType is a supertype or the same as ParentMemberNodeType 586 // this way we could e.g. enforce a child of a Table component to be <tr> 587 import libwasm.bindings.HTMLElement : HTMLElement; 588 589 static assert(is(ChildMemberNodeType : HTMLElement)); 590 auto ptr = &__traits(getMember, ts[index], injected_name).node; 591 __traits(getMember, t, local_name) = cast(ChildMemberType) ptr; 592 } 593 } 594 595 void verifyChildParams(Super, string parentField, Params...)() 596 { 597 import std.traits : hasUDA; 598 599 alias ParamsTuple = staticMap!(TemplateArgsOf, Params); 600 static foreach (Param; AliasSeq!ParamsTuple) 601 { 602 { 603 enum childName = __traits(identifier, Param.Field); 604 alias ChildMemberType = typeof(Param.Field); 605 static if (!isBasicType!(ChildMemberType) && !isValue!(Param.Field) && hasMember!(Super, childName)) 606 { 607 enum childOffset = __traits(getMember, Super, childName).offsetof; 608 enum parentOffset = __traits(getMember, Super, parentField).offsetof; 609 610 // To make sure we don't render injected fields 611 static assert(!hasUDA!(__traits(getMember, Super, childName), child), "Injected member '" ~ childName ~ "' cannot have @child attribute in struct " ~ Super 612 .stringof); 613 614 // To make sure we don't inject an injected field 615 // Todo: Omit @inject fields from the tuple 616 static assert(childOffset > parentOffset, "Injected member '" ~ childName ~ "' needs to be declared after '" ~ parentField ~ "' in struct " ~ Super 617 .stringof); 618 } 619 } 620 } 621 } 622 623 // todo: loadFromHTML 624 // parse DOM, iterate structs to find element placement, set values, recur for each child 625 // if the child list is equivalent to the parsed dom children, populate each child and attributes 626 // otherwise, build an array with the missing child nodes, and match the children using the ID while verifying the ordering 627 // if the struct is missing attributes, build an array with the missing attributes 628 // if all attributes are missing and there are no children in the struct, use innerHTML 629 // call remount after this is done 630 // todo: create the logic in render() 631 // iterate the children and attributes arrays to load the ones that were missing 632 // use a placement array to set the position relative to existing children nodes 633 634 // Populates the injected fields 635 auto compile(T, Ts...)(return auto ref T t, return auto ref Ts ts) @trusted 636 { 637 import std.meta : AliasSeq; 638 import std.traits : hasUDA; 639 640 static foreach (sym; T.tupleof) 641 { 642 { 643 static if (isPointer!(typeof(sym))) 644 alias ChildType = PointerTarget!(typeof(sym)); 645 else 646 alias ChildType = typeof(sym); 647 enum i = sym.stringof; // TODO: can this be fieldName = __traits(identifier, sym) instead of i = sym.stringof ?? 648 enum isImmutable = is(typeof(sym) : immutable(T), T); 649 enum isPublic = __traits(getProtection, sym) == "public"; 650 651 static if (!isImmutable) 652 { 653 654 static if (is(ChildType : NamedNode!name, string name)) 655 { 656 static if (i != "node") 657 { // reference to a parent struct 658 setChildFromParent!(__traits(identifier, sym), __traits(identifier, sym))(t, ts); 659 660 alias udas = getUDAs!(sym, inject); 661 static foreach (uda; udas) 662 { 663 static if (is(uda == inject!ref_name, string ref_name)) 664 { 665 setChildFromParent!(ref_name, __traits(identifier, sym))(t, ts); 666 break; 667 } 668 } 669 } 670 } 671 else 672 { 673 static if (isPublic) 674 { // reference to a parent member 675 676 setParamFromParent!(i, i)(t, ts); 677 alias udas = getUDAs!(sym, inject); 678 static foreach (uda; udas) 679 { 680 static if (is(uda == inject!ref_name, string ref_name)) 681 { 682 setParamFromParent!(ref_name, i)(t, ts); 683 break; 684 } 685 } 686 } 687 static if (isPublic && !is(sym) && isAggregateType!T) 688 { 689 690 static if (is(typeof(sym) : DynamicArray!(Item), Item)) 691 { 692 // items in appenders need to be set via render functions 693 } 694 else 695 { 696 static if (!isCallable!(typeof(sym))) 697 { //} && !isPointer!(typeof(sym))) { 698 699 import libwasm.spa; 700 701 alias Params = getUDAs!(sym, Parameters); 702 static if (Params.length > 0) 703 { 704 verifyChildParams!(T, i, Params); 705 auto params = createParameterTuple!(Params)(AliasSeq!(t, ts)); 706 } 707 else 708 alias params = AliasSeq!(); 709 static if (isAggregateType!(ChildType)) 710 { // recurse through the child members 711 712 static if (isPointer!(typeof(sym))) 713 compile(*__traits(getMember, t, i), AliasSeq!(params, t, ts)); 714 else 715 compile(__traits(getMember, t, i), AliasSeq!(params, t, ts)); 716 } 717 } 718 } 719 } 720 } 721 } 722 } 723 } 724 725 // adds too much complexity? onMount / onUnmount would be better? 726 727 static if (hasMember!(T, "construct")) 728 t.construct(); 729 730 } 731 732 // todo: addRouterHandlers 733 // todo: removeRouterHandlers 734 735 auto isChildVisible(string child, Parent)(return auto ref Parent parent) 736 { 737 alias visiblePreds = getSymbolsByUDA!(Parent, visible); 738 static foreach (sym; visiblePreds) 739 { 740 { 741 alias vs = getUDAs!(sym, visible); 742 // TODO: static assert sym is callable 743 static foreach (v; vs) 744 { 745 { 746 static if (is(v == visible!name, string name) && child == name) 747 { 748 static if (is(typeof(sym) == bool)) 749 { 750 if (!__traits(getMember, parent, __traits(identifier, sym))) 751 return false; 752 } 753 else 754 { 755 auto result = callMember!(__traits(identifier, sym))(parent); 756 if (result == false) 757 return false; 758 } 759 } 760 } 761 } 762 } 763 } 764 return true; 765 } 766 767 auto callMember(string fun, T)(return auto ref T t) 768 { 769 import memutils.ct : ParameterIdentifierTuple; 770 import std.meta : staticMap, AliasSeq; 771 772 alias params = ParameterIdentifierTuple!(__traits(getMember, t, fun)); 773 static if (params.length == 0) 774 { 775 return __traits(getMember, t, fun)(); 776 } 777 else static if (params.length == 1) 778 { 779 return __traits(getMember, t, fun)(__traits(getMember, t, params[0])); 780 } 781 else static if (params.length == 2) 782 { 783 return __traits(getMember, t, fun)(__traits(getMember, t, params[0]), __traits(getMember, t, params[1])); 784 } 785 else 786 { 787 static assert(false, "Not implemented"); 788 } 789 } 790 791 auto renderIntoNode(T, Ts...)(Handle parent, return auto ref T t, return auto ref Ts ts) 792 if (isPointer!T) 793 { 794 return renderIntoNode(parent, *t, ts); 795 } 796 797 ref auto getNamedNode(T)(return auto ref T t) if (isPointer!T) 798 { 799 return (*t).getNamedNode(); 800 } 801 802 ref auto getNamedNode(T)(return auto ref T t) if (!isPointer!T) 803 { 804 import libwasm.node : NamedNode; 805 806 static if (is(T : NamedNode!name, string name)) 807 { 808 return t; 809 } 810 else static if (hasMember!(T, "node")) 811 { 812 return t.node; 813 } 814 else 815 { 816 alias children = getSymbolsByUDA!(T, child); 817 static assert(children.length == 1); 818 return __traits(getMember, t, __traits(identifier, children[0])).getNamedNode(); 819 } 820 } 821 822 template createNestedChildRenderFuncs(string memberName) 823 { 824 auto createNestedChildRenderFuncs(T)(return auto ref T t) 825 { 826 import std.traits : hasUDA; 827 import std.typecons : tuple; 828 829 template isNestedChild(alias member) 830 { 831 alias MemberType = typeof(member); 832 static if (!__traits(hasMember, MemberType, "node")) 833 enum isNestedChild = false; 834 else 835 enum isNestedChild = is(typeof(MemberType.node) : NamedNode!type, string type); 836 } 837 838 template extractName(alias param) 839 { 840 alias extractName = param.Name; 841 } 842 843 template extractField(alias param) 844 { 845 alias extractField = param.Field; 846 } 847 848 template isFieldNestedChild(alias param) 849 { 850 enum isFieldNestedChild = isNestedChild!(extractField!(param)); 851 } 852 853 auto createRenderFunc(alias param)() @trusted 854 { 855 enum nestedChildName = __traits(identifier, extractField!(param)); 856 return cast(NestedChildRenderFunc)(Handle parent) @safe { 857 auto nestedChildRenderFuncs = .createNestedChildRenderFuncs!(nestedChildName)(t); 858 render(parent, __traits(getMember, t, nestedChildName), nestedChildRenderFuncs); 859 }; 860 } 861 862 alias ParamsTuple = getAnnotatedParameters!(__traits(getMember, T, memberName)); 863 alias ParamFieldsTuple = staticMap!(extractField, ParamsTuple); 864 alias nestedChildParams = Filter!(isFieldNestedChild, ParamsTuple); 865 alias Names = staticMap!(extractName, nestedChildParams); 866 static if (Names.length == 0) 867 return tuple(); 868 else 869 return tuple!(Names)(staticMap!(createRenderFunc, nestedChildParams)); 870 } 871 } 872 873 alias NestedChildRenderFunc = void delegate(uint) nothrow @safe; 874 875 template renderNestedChild(string field) 876 { 877 template hasRenderFunc(alias T) 878 { 879 static if (!hasMember!(T, field)) 880 enum hasRenderFunc = false; 881 else 882 { 883 alias fieldType = typeof(__traits(getMember, T, field)); 884 enum hasRenderFunc = is(fieldType : NestedChildRenderFunc); 885 } 886 } 887 888 auto renderNestedChild(T, Ts...)(Handle parent, return auto ref T t, return auto ref Ts ts) 889 { 890 enum tupleIndex = indexOfPred!(hasRenderFunc, Ts); 891 static if (tupleIndex != -1) 892 { 893 __traits(getMember, ts[tupleIndex], field)(parent); 894 } 895 } 896 } 897 // NOTE: only trusted because of the one __gshared thing 898 @trusted auto renderIntoNode(T, Ts...)(Handle parent, return auto ref T t, return auto ref Ts ts) 899 if (!isPointer!T) 900 { 901 import std.traits : hasUDA, getUDAs; 902 import std.meta : AliasSeq; 903 import std.meta : staticMap; 904 import std.traits : isCallable, getSymbolsByUDA, isPointer; 905 906 enum hasNode = hasMember!(T, "node"); 907 static if (hasNode) 908 { 909 bool shouldRender = t.getNamedNode().node == invalidHandle; 910 } 911 else 912 bool shouldRender = true; 913 if (shouldRender) 914 { 915 auto node = createNode(parent, t); 916 static if (hasMember!(T, "node")) 917 { 918 t.getNamedNode().node.handle.handle = node; 919 } 920 alias StyleSet = getStyleSet!T; 921 static foreach (sym; T.tupleof) 922 { 923 { 924 enum i = sym.stringof; 925 alias name = getSymbolCustomName!(sym, domName!i); 926 static if (!is(sym)) 927 { 928 alias styles = getStyles!(sym); 929 static if (hasUDA!(sym, child)) 930 { 931 import libwasm.spa; 932 933 alias params = AliasSeq!(); 934 if (isChildVisible!(i)(t)) 935 { 936 static if (is(typeof(sym) : DynamicArray!(Item*), Item)) 937 { 938 foreach (ref item; __traits(getMember, t, i)) 939 { 940 node.render(*item, AliasSeq!(params)); 941 static if (is(typeof(t) == HTMLArray!Item)) 942 t.assignEventListeners(*item); 943 } 944 } 945 else static if (is(typeof(sym) : NamedNode!(name)*, string name)) 946 { 947 renderNestedChild!(i)(node, t, ts); 948 } 949 else 950 { 951 auto nestedChildRenderFuncs = createNestedChildRenderFuncs!(i)(t); 952 node.render(__traits(getMember, t, i), AliasSeq!(params, nestedChildRenderFuncs)); 953 } 954 } 955 } 956 else static if (hasUDA!(sym, prop)) 957 { 958 node.setPropertyTyped!name(__traits(getMember, t, i)); 959 } 960 else static if (hasUDA!(sym, attr)) 961 { 962 node.setAttributeTyped!name(__traits(getMember, t, i)); 963 } 964 965 alias extendedStyles = getStyleSets!(sym); 966 static foreach (style; extendedStyles) 967 { 968 static assert(hasMember!(typeof(sym), "node"), "styleset on field is currently only possible when said field has a Node mixin"); 969 970 __traits(getMember, t, i).node.setAttribute(GenerateExtendedStyleSetName!style, ""); 971 } 972 static if (i == "node") 973 { 974 node.applyStyles!(T, styles); 975 } 976 else static if (styles.length > 0) 977 { 978 static if (isCallable!(sym)) 979 { 980 auto result = callMember!(i)(t); 981 if (result == true) 982 { 983 t.getNamedNode.applyStyles!(T, styles); 984 } 985 } 986 else static if (is(typeof(sym) == bool)) 987 { 988 if (__traits(getMember, t, i) == true) 989 t.getNamedNode.applyStyles!(T, styles); 990 } 991 else static if (hasUDA!(sym, child)) 992 { 993 getNamedNode(__traits(getMember, t, i)).applyStyles!(T, styles); 994 } 995 } 996 } 997 } 998 } 999 static foreach (i; __traits(allMembers, T)) 1000 { 1001 { 1002 static if (!is(__traits(getMember, T, i)) && isCallable!(__traits(getMember, T, i))) 1003 { 1004 alias sym = AliasSeq!(__traits(getMember, T, i))[0]; 1005 1006 alias name = getSymbolCustomName!(sym, domName!i); 1007 static if (hasUDA!(sym, child)) 1008 static assert(false, "we don't support @child functions"); 1009 else static if (hasUDA!(sym, prop)) 1010 { 1011 auto result = callMember!(i)(t); 1012 t.getNamedNode.setPropertyTyped!name(result); 1013 } 1014 else static if (hasUDA!(sym, callback)) 1015 { 1016 t.getNamedNode.addEventListenerTyped!i(t); 1017 } 1018 else static if (hasUDA!(sym, attr)) 1019 { 1020 auto result = callMember!(i)(t); 1021 t.getNamedNode.setAttributeTyped!name(result); 1022 } 1023 else static if (hasUDA!(sym, style)) 1024 { 1025 auto result = callMember!(i)(t); 1026 static foreach (style; getStyles!(sym)) 1027 { 1028 __gshared static string className = GetCssClassName!(T, style); 1029 t.getNamedNode.changeClass(className, result); 1030 } 1031 } 1032 else static if (hasUDA!(sym, connect)) 1033 { 1034 alias connects = getUDAs!(sym, connect); 1035 static foreach (c; connects) 1036 { 1037 auto del = &__traits(getMember, t, i); 1038 static if (is(c : connect!(a, b), alias a, alias b)) 1039 { 1040 mixin("t." ~ a ~ "." ~ replace!(b, '.', '_') ~ ".add(del);"); 1041 } 1042 else static if (is(c : connect!field, alias field)) 1043 { 1044 static assert(__traits(compiles, mixin("t." ~ field)), "Cannot find property " ~ field ~ " on " ~ T 1045 .stringof ~ " in @connect"); 1046 mixin("t." ~ field ~ ".add(del);"); 1047 } 1048 else static if (is(c : connect!field, string field)) 1049 { 1050 mixin("t." ~ field ~ ".add(del);"); 1051 } 1052 } 1053 } 1054 } 1055 } 1056 } 1057 1058 alias enumsWithApplyStyles = getSymbolsByUDA!(T, ApplyStyle); 1059 static foreach (member; enumsWithApplyStyles) 1060 { 1061 { 1062 alias ApplyStyles = getUDAs!(member, ApplyStyle); 1063 static foreach (s; ApplyStyles) 1064 { 1065 static if (is(s == ApplyStyle!Target, alias Target)) 1066 { 1067 static if (isFunction!member) 1068 alias EnumType = ReturnType!member; 1069 else 1070 alias EnumType = typeof(member); 1071 alias GetUDAs = ApplyRight!(ApplyLeft!(getEnumUDAs, EnumType), style); 1072 alias table = staticMap!(GetUDAs, __traits(allMembers, EnumType)); 1073 alias GetCssClass = ApplyLeft!(GetCssClassName, T); 1074 alias classes = staticMap!(GetCssClass, staticMap!(extractStyleStruct, table)); 1075 enum string[classes.length] styleTable = [classes]; 1076 size_t idx = __traits(getMember, t, __traits(identifier, member)); 1077 static if (__traits(identifier, Target) == "node") 1078 { 1079 node.addClass(styleTable[idx]); 1080 } 1081 else static if (hasUDA!(Target, child)) 1082 { 1083 __traits(getMember, t, __traits(identifier, Target)).getNamedNode.addClass( 1084 styleTable[idx]); 1085 } 1086 else 1087 static assert(false, "Can only have ApplyStyle point to node or to a child component"); 1088 } 1089 } 1090 } 1091 } 1092 } 1093 } 1094 1095 template getSymbolCustomName(alias symbol, string defaultName) 1096 { 1097 alias names = getStringUDAs!(symbol); 1098 static if (names.length > 0) 1099 enum getSymbolCustomName = names[0]; 1100 else 1101 enum getSymbolCustomName = defaultName; 1102 } 1103 1104 template getEnumUDAs(EnumType, string field, alias UDA) 1105 { 1106 alias udas = AliasSeq!(__traits(getAttributes, __traits(getMember, EnumType, field))); 1107 alias getEnumUDAs = Filter!(isDesiredUDA!UDA, udas); 1108 } 1109 1110 private template isDesiredUDA(alias attribute) 1111 { 1112 template isDesiredUDA(alias toCheck) 1113 { 1114 static if (is(typeof(attribute)) && !__traits(isTemplate, attribute)) 1115 { 1116 static if (__traits(compiles, toCheck == attribute)) 1117 enum isDesiredUDA = toCheck == attribute; 1118 else 1119 enum isDesiredUDA = false; 1120 } 1121 else static if (is(typeof(toCheck))) 1122 { 1123 static if (__traits(isTemplate, attribute)) 1124 enum isDesiredUDA = isInstanceOf!(attribute, typeof(toCheck)); 1125 else 1126 enum isDesiredUDA = is(typeof(toCheck) == attribute); 1127 } 1128 else static if (__traits(isTemplate, attribute)) 1129 enum isDesiredUDA = isInstanceOf!(attribute, toCheck); 1130 else 1131 enum isDesiredUDA = is(toCheck == attribute); 1132 } 1133 } 1134 1135 template hasMemberWithInjectOf(string field, alias c) 1136 { 1137 enum bool hasMemberWithInjectOf = () { 1138 alias ChildType = typeof(c); 1139 alias getSymbol = ApplyLeft!(getMember, c); 1140 alias childrenNames = getChildren!ChildType; 1141 alias children = staticMap!(getSymbol, childrenNames); 1142 static foreach (c; children) 1143 { 1144 alias udas = getUDAs!(c, inject); 1145 static foreach (uda; udas) 1146 { 1147 static if (is(uda == inject!ref_name, string ref_name)) 1148 { 1149 static if (ref_name == field) 1150 { 1151 return true; 1152 } 1153 } 1154 } 1155 } 1156 return false; 1157 }(); 1158 } 1159 1160 template among(alias field, T...) 1161 { 1162 static if (T.length == 0) 1163 enum among = false; 1164 else static if (T.length == 1) 1165 enum among = field.stringof == T[0]; 1166 else 1167 enum among = among!(field, T[0 .. $ / 2]) || among!(field, T[$ / 2 .. $]); 1168 } 1169 1170 template getAnnotatedParameters(alias symbol) 1171 { 1172 import libwasm.spa; 1173 1174 alias Params = getUDAs!(symbol, Parameters); 1175 alias getAnnotatedParameters = staticMap!(TemplateArgsOf, Params); 1176 } 1177 1178 // todo: Test if this works for @inject 1179 template updateChildren(alias member) 1180 { 1181 enum field = __traits(identifier, member); 1182 alias Source = AliasSeq!(__traits(parent, member))[0]; 1183 template isParamField(Param) 1184 { 1185 enum isParamField = TemplateArgsOf!(Param)[1].stringof == field; 1186 } 1187 1188 static void updateChildren(Parent)(return auto ref Parent parent) 1189 { 1190 // we are updating field in parent 1191 // all children that have a pointer with the exact same name 1192 // should get an update 1193 // all children that have a params annotation that refers to the field 1194 // should get an update 1195 import std.traits : getSymbolsByUDA; 1196 import std.meta : ApplyLeft, staticMap; 1197 1198 static if (isPointer!(Parent)) 1199 alias ParentType = PointerTarget!(Parent); 1200 else 1201 alias ParentType = Parent; 1202 1203 // look for functions using the field as a parameter 1204 static foreach (t; ParentType.tupleof) 1205 { 1206 static foreach (param; getAnnotatedParameters!(t)) 1207 { 1208 static if (!isValue!(TemplateArgsOf!(param)[1])) 1209 { 1210 alias target = TemplateArgsOf!(param)[1]; 1211 static if (__traits(isSame, target, member)) 1212 { 1213 __traits(getMember, parent, __traits(identifier, t)).update!(__traits(getMember, __traits(getMember, parent, __traits( 1214 identifier, t)), param.Name)); 1215 } 1216 } 1217 } 1218 } 1219 1220 // propagate through members with a @child UDA 1221 alias getSymbol = ApplyLeft!(getMember, parent); 1222 alias childrenNames = getChildren!Parent; 1223 alias children = staticMap!(getSymbol, childrenNames); 1224 static foreach (c; children) 1225 { 1226 { 1227 alias ChildType = typeof(c); 1228 1229 static if (hasMember!(ChildType, field)) 1230 { 1231 __traits(getMember, parent, c.stringof).update!(__traits(getMember, __traits(getMember, parent, c 1232 .stringof), field)); 1233 } 1234 else static if (hasMemberWithInjectOf!(field, c)) 1235 { 1236 __traits(getMember, parent, c.stringof).update!(__traits(getMember, __traits(getMember, parent, c 1237 .stringof), field)); 1238 } 1239 else 1240 1241 1242 1243 .updateChildren!(member)(__traits(getMember, parent, c.stringof)); 1244 } 1245 } 1246 } 1247 } 1248 1249 auto update(T)(ref T node) if (hasMember!(T, "node")) 1250 { 1251 struct Inner 1252 { 1253 nothrow: 1254 void opDispatch(string name, V)(auto ref V v) const @safe 1255 { 1256 mixin("node.update!(node." ~ name ~ ")(v);"); 1257 // NOTE: static assert won't work in opDispatch, as the compiler will not output the string but just ignore the opDispatch call and error out saying missing field on Inner 1258 static if (!hasMember!(T, name)) 1259 pragma(msg, "********* Error: " ~ T.stringof ~ " has no property named " ~ name); 1260 } 1261 } 1262 1263 return Inner(); 1264 } 1265 1266 void update(Range, Sink)(auto ref Range source, ref Sink sink) 1267 { 1268 1269 auto output = Updater!(Sink)(&sink); 1270 foreach (i; source) 1271 output.put(i); 1272 } 1273 1274 void setVisible(string field, Parent)(auto ref Parent parent, bool visible) 1275 { 1276 bool current = __traits(getMember, parent, field).node.mounted; 1277 if (current != visible) 1278 { 1279 if (visible) 1280 { 1281 remount!(field)(parent); 1282 } 1283 else 1284 { 1285 unmount(__traits(getMember, parent, field)); 1286 } 1287 } 1288 } 1289 1290 // this will propagate through all the triggers to see if the update caused a state change 1291 @trusted 1292 template update(alias field) 1293 { 1294 import std.traits : isPointer; 1295 1296 static void updateDom(Parent, T)(auto ref Parent parent, auto ref T t) 1297 { 1298 import memutils.ct : ParameterIdentifierTuple; 1299 import std.traits : hasUDA, isCallable, getUDAs; 1300 import std.meta : AliasSeq; 1301 import std.meta : staticMap; 1302 1303 alias name = domName!(field.stringof); 1304 static if (hasUDA!(field, prop)) 1305 { 1306 parent.node.setPropertyTyped!name(t); 1307 } 1308 else static if (hasUDA!(field, attr)) 1309 { 1310 parent.node.setAttributeTyped!name(t); 1311 } 1312 static if (is(T == bool)) 1313 { 1314 alias styles = getStyles!(field); 1315 static foreach (style; styles) 1316 { 1317 __gshared static string className = GetCssClassName!(Parent, style); 1318 parent.getNamedNode.changeClass(className, t); 1319 } 1320 static if (hasUDA!(field, visible)) 1321 { 1322 alias udas = getUDAs!(field, visible); 1323 static foreach (uda; udas) 1324 { 1325 static if (is(uda : visible!elem, alias elem)) 1326 { 1327 // callMember behind the scenes 1328 setVisible!(elem)(parent, __traits(getMember, parent, __traits(identifier, field))); 1329 } 1330 } 1331 } 1332 } 1333 static foreach (i; __traits(allMembers, Parent)) 1334 { 1335 { 1336 alias sym = AliasSeq!(__traits(getMember, parent, i))[0]; 1337 static if (isCallable!(sym)) 1338 { 1339 alias params = ParameterIdentifierTuple!sym; 1340 static if (among!(field, params)) 1341 { 1342 static if (hasUDA!(sym, prop)) 1343 { 1344 alias cleanName = domName!i; 1345 auto result = callMember!(i)(parent); 1346 parent.node.node.setPropertyTyped!cleanName(result); 1347 } 1348 else static if (hasUDA!(sym, attr)) 1349 { 1350 alias cleanName = domName!i; 1351 auto result = callMember!(i)(parent); 1352 parent.node.node.setAttributeTyped!cleanName(result); 1353 } 1354 else static if (hasUDA!(sym, style)) 1355 { 1356 auto result = callMember!(i)(parent); 1357 static foreach (style; getStyles!(sym)) 1358 { 1359 __gshared static string className = GetCssClassName!(Parent, style); 1360 parent.node.node.changeClass(className, result); 1361 } 1362 } 1363 else 1364 { 1365 import std.traits : ReturnType; 1366 1367 alias RType = ReturnType!(__traits(getMember, parent, i)); 1368 static if (is(RType : void)) 1369 callMember!(i)(parent); 1370 else 1371 { 1372 auto result = callMember!(i)(parent); 1373 static if (hasUDA!(sym, visible)) 1374 { 1375 alias udas = getUDAs!(sym, visible); 1376 static foreach (uda; udas) 1377 { 1378 static if (is(uda : visible!elem, alias elem)) 1379 { 1380 setVisible!(elem)(parent, result); 1381 } 1382 } 1383 } 1384 } 1385 } 1386 } 1387 } 1388 } 1389 } 1390 updateChildren!(field)(parent); 1391 } 1392 1393 static void update(Parent)(scope auto ref Parent parent) 1394 { 1395 if (!parent.node.mounted) 1396 return; 1397 static if (isPointer!Parent) 1398 updateDom(*parent, __traits(getMember, parent, field.stringof)); 1399 else 1400 updateDom(parent, __traits(getMember, parent, field.stringof)); 1401 } 1402 1403 static void update(Parent, T)(scope auto ref Parent parent, T t) 1404 { 1405 mixin("parent." ~ field.stringof ~ " = t;"); 1406 if (!parent.node.mounted) 1407 return; 1408 static if (isPointer!Parent) 1409 updateDom(*parent, t); 1410 else 1411 updateDom(parent, t); 1412 } 1413 } 1414 1415 template symbolFromAliasThis(Parent, string name) 1416 { 1417 import std.meta : anySatisfy; 1418 1419 alias aliasThises = AliasSeq!(__traits(getAliasThis, Parent)); 1420 static if (aliasThises.length == 0) 1421 enum symbolFromAliasThis = false; 1422 else 1423 { 1424 alias hasSymbol = ApplyRight!(hasMember, name); 1425 enum symbolFromAliasThis = anySatisfy!(hasSymbol, aliasThises); 1426 } 1427 } 1428 1429 auto setAttributeTyped(string name, T)(Handle node, auto ref T t) 1430 { 1431 import std.traits : isPointer; 1432 1433 static if (isPointer!T) 1434 { 1435 if (t !is null) 1436 node.setAttributeTyped!name(*t); 1437 } 1438 else static if (is(T == bool)) 1439 node.setAttributeBool(name, t); 1440 else static if (is(T : int)) 1441 { 1442 node.setAttributeInt(name, t); 1443 } 1444 else 1445 { 1446 node.setAttribute(name, t); 1447 } 1448 } 1449 1450 auto setPropertyTyped(string name, T)(Handle node, auto ref T t) 1451 { 1452 import std.traits : isPointer, isNumeric; 1453 1454 static if (isPointer!T) 1455 { 1456 if (t !is null) 1457 node.setPropertyTyped!name(*t); 1458 } 1459 else static if (is(T == bool)) 1460 node.setPropertyBool(name, t); 1461 else static if (isIntegral!(T)) 1462 node.setPropertyInt(name, t); 1463 else static if (isNumeric!(T)) 1464 node.setPropertyDouble(name, t); 1465 else 1466 { 1467 static if (__traits(compiles, __traits(getMember, api, name))) 1468 __traits(getMember, api, name)(node, t); 1469 else 1470 node.setProperty(name, t); 1471 } 1472 } 1473 1474 auto applyStyles(T, styles...)(Handle node) 1475 { 1476 static foreach (style; styles) 1477 { 1478 node.addClass(GetCssClassName!(T, style)); 1479 } 1480 } 1481 1482 Handle createNode(T)(Handle parent, ref T t) 1483 { 1484 enum hasNode = hasMember!(T, "node"); 1485 static if (hasNode && is(typeof(t.node) : NamedNode!tag, alias tag)) 1486 { 1487 mixin("NodeType n = NodeType." ~ tag ~ ";"); 1488 return createElement(n); 1489 } 1490 else 1491 return parent; 1492 } 1493 1494 template indexOfPred(alias Pred, TList...) 1495 { 1496 enum indexOfPred = indexOf!(Pred, TList).index; 1497 } 1498 1499 template indexOf(alias Pred, args...) 1500 { 1501 import std.meta : AliasSeq; 1502 1503 static if (args.length > 0) 1504 { 1505 static if (Pred!(args[0])) 1506 { 1507 enum index = 0; 1508 } 1509 else 1510 { 1511 enum next = indexOf!(Pred, AliasSeq!(args[1 .. $])).index; 1512 enum index = (next == -1) ? -1 : 1 + next; 1513 } 1514 } 1515 else 1516 { 1517 enum index = -1; 1518 } 1519 } 1520 1521 template domName(string name) 1522 { 1523 static if (name[$ - 1] == '_') 1524 enum domName = name[0 .. $ - 1]; 1525 else 1526 enum domName = name; 1527 } 1528 1529 template join(Seq...) 1530 { 1531 static if (is(typeof(Seq) == string)) 1532 enum join = Seq; 1533 else 1534 { 1535 static if (Seq.length == 1) 1536 { 1537 enum join = Seq[0]; 1538 } 1539 else 1540 { 1541 enum join = Seq[0] ~ "," ~ join!(Seq[1 .. $]); 1542 } 1543 } 1544 }