forked from dylans/dojo
-
Notifications
You must be signed in to change notification settings - Fork 0
/
parser.js
920 lines (820 loc) · 34.3 KB
/
parser.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
define([
"require", "./_base/kernel", "./_base/lang", "./_base/array", "./_base/config", "./dom", "./_base/window",
"./_base/url", "./aspect", "./promise/all", "./date/stamp", "./Deferred", "./has", "./query", "./on", "./ready"
], function(require, dojo, dlang, darray, config, dom, dwindow, _Url, aspect, all, dates, Deferred, has, query, don, ready){
// module:
// dojo/parser
new Date("X"); // workaround for #11279, new Date("") == NaN
// data-dojo-props etc. is not restricted to JSON, it can be any javascript
function myEval(text){
return eval("(" + text + ")");
}
// Widgets like BorderContainer add properties to _Widget via dojo.extend().
// If BorderContainer is loaded after _Widget's parameter list has been cached,
// we need to refresh that parameter list (for _Widget and all widgets that extend _Widget).
var extendCnt = 0;
aspect.after(dlang, "extend", function(){
extendCnt++;
}, true);
function getNameMap(ctor){
// summary:
// Returns map from lowercase name to attribute name in class, ex: {onclick: "onClick"}
var map = ctor._nameCaseMap, proto = ctor.prototype;
// Create the map if it's undefined.
// Refresh the map if a superclass was possibly extended with new methods since the map was created.
if(!map || map._extendCnt < extendCnt){
map = ctor._nameCaseMap = {};
for(var name in proto){
if(name.charAt(0) === "_"){
continue;
} // skip internal properties
map[name.toLowerCase()] = name;
}
map._extendCnt = extendCnt;
}
return map;
}
// Map from widget name or list of widget names(ex: "dijit/form/Button,acme/MyMixin") to a constructor.
var _ctorMap = {};
function getCtor(/*String[]*/ types, /*Function?*/ contextRequire){
// summary:
// Retrieves a constructor. If the types array contains more than one class/MID then the
// subsequent classes will be mixed into the first class and a unique constructor will be
// returned for that array.
var ts = types.join();
if(!_ctorMap[ts]){
var mixins = [];
for(var i = 0, l = types.length; i < l; i++){
var t = types[i];
// TODO: Consider swapping getObject and require in the future
mixins[mixins.length] = (_ctorMap[t] = _ctorMap[t] || (dlang.getObject(t) || (~t.indexOf('/') &&
(contextRequire ? contextRequire(t) : require(t)))));
}
var ctor = mixins.shift();
_ctorMap[ts] = mixins.length ? (ctor.createSubclass ? ctor.createSubclass(mixins) : ctor.extend.apply(ctor, mixins)) : ctor;
}
return _ctorMap[ts];
}
var parser = {
// summary:
// The Dom/Widget parsing package
_clearCache: function(){
// summary:
// Clear cached data. Used mainly for benchmarking.
extendCnt++;
_ctorMap = {};
},
_functionFromScript: function(script, attrData){
// summary:
// Convert a `<script type="dojo/method" args="a, b, c"> ... </script>`
// into a function
// script: DOMNode
// The `<script>` DOMNode
// attrData: String
// For HTML5 compliance, searches for attrData + "args" (typically
// "data-dojo-args") instead of "args"
var preamble = "",
suffix = "",
argsStr = (script.getAttribute(attrData + "args") || script.getAttribute("args")),
withStr = script.getAttribute("with");
// Convert any arguments supplied in script tag into an array to be passed to the
var fnArgs = (argsStr || "").split(/\s*,\s*/);
if(withStr && withStr.length){
darray.forEach(withStr.split(/\s*,\s*/), function(part){
preamble += "with(" + part + "){";
suffix += "}";
});
}
return new Function(fnArgs, preamble + script.innerHTML + suffix);
},
instantiate: function(nodes, mixin, options){
// summary:
// Takes array of nodes, and turns them into class instances and
// potentially calls a startup method to allow them to connect with
// any children.
// nodes: Array
// Array of DOM nodes
// mixin: Object?
// An object that will be mixed in with each node in the array.
// Values in the mixin will override values in the node, if they
// exist.
// options: Object?
// An object used to hold kwArgs for instantiation.
// See parse.options argument for details.
// returns:
// Array of instances.
mixin = mixin || {};
options = options || {};
var dojoType = (options.scope || dojo._scopeName) + "Type", // typically "dojoType"
attrData = "data-" + (options.scope || dojo._scopeName) + "-", // typically "data-dojo-"
dataDojoType = attrData + "type", // typically "data-dojo-type"
dataDojoMixins = attrData + "mixins"; // typically "data-dojo-mixins"
var list = [];
darray.forEach(nodes, function(node){
var type = dojoType in mixin ? mixin[dojoType] : node.getAttribute(dataDojoType) || node.getAttribute(dojoType);
if(type){
var mixinsValue = node.getAttribute(dataDojoMixins),
types = mixinsValue ? [type].concat(mixinsValue.split(/\s*,\s*/)) : [type];
list.push({
node: node,
types: types
});
}
});
// Instantiate the nodes and return the list of instances.
return this._instantiate(list, mixin, options);
},
_instantiate: function(nodes, mixin, options, returnPromise){
// summary:
// Takes array of objects representing nodes, and turns them into class instances and
// potentially calls a startup method to allow them to connect with
// any children.
// nodes: Array
// Array of objects like
// | {
// | ctor: Function (may be null)
// | types: ["dijit/form/Button", "acme/MyMixin"] (used if ctor not specified)
// | node: DOMNode,
// | scripts: [ ... ], // array of <script type="dojo/..."> children of node
// | inherited: { ... } // settings inherited from ancestors like dir, theme, etc.
// | }
// mixin: Object
// An object that will be mixed in with each node in the array.
// Values in the mixin will override values in the node, if they
// exist.
// options: Object
// An options object used to hold kwArgs for instantiation.
// See parse.options argument for details.
// returnPromise: Boolean
// Return a Promise rather than the instance; supports asynchronous widget creation.
// returns:
// Array of instances, or if returnPromise is true, a promise for array of instances
// that resolves when instances have finished initializing.
// Call widget constructors. Some may be asynchronous and return promises.
var thelist = darray.map(nodes, function(obj){
var ctor = obj.ctor || getCtor(obj.types, options.contextRequire);
// If we still haven't resolved a ctor, it is fatal now
if(!ctor){
throw new Error("Unable to resolve constructor for: '" + obj.types.join() + "'");
}
return this.construct(ctor, obj.node, mixin, options, obj.scripts, obj.inherited);
}, this);
// After all widget construction finishes, call startup on each top level instance if it makes sense (as for
// widgets). Parent widgets will recursively call startup on their (non-top level) children
function onConstruct(thelist){
if(!mixin._started && !options.noStart){
darray.forEach(thelist, function(instance){
if(typeof instance.startup === "function" && !instance._started){
instance.startup();
}
});
}
return thelist;
}
if(returnPromise){
return all(thelist).then(onConstruct);
}else{
// Back-compat path, remove for 2.0
return onConstruct(thelist);
}
},
construct: function(ctor, node, mixin, options, scripts, inherited){
// summary:
// Calls new ctor(params, node), where params is the hash of parameters specified on the node,
// excluding data-dojo-type and data-dojo-mixins. Does not call startup().
// ctor: Function
// Widget constructor.
// node: DOMNode
// This node will be replaced/attached to by the widget. It also specifies the arguments to pass to ctor.
// mixin: Object?
// Attributes in this object will be passed as parameters to ctor,
// overriding attributes specified on the node.
// options: Object?
// An options object used to hold kwArgs for instantiation. See parse.options argument for details.
// scripts: DomNode[]?
// Array of `<script type="dojo/*">` DOMNodes. If not specified, will search for `<script>` tags inside node.
// inherited: Object?
// Settings from dir=rtl or lang=... on a node above this node. Overrides options.inherited.
// returns:
// Instance or Promise for the instance, if markupFactory() itself returned a promise
var proto = ctor && ctor.prototype;
options = options || {};
// Setup hash to hold parameter settings for this widget. Start with the parameter
// settings inherited from ancestors ("dir" and "lang").
// Inherited setting may later be overridden by explicit settings on node itself.
var params = {};
if(options.defaults){
// settings for the document itself (or whatever subtree is being parsed)
dlang.mixin(params, options.defaults);
}
if(inherited){
// settings from dir=rtl or lang=... on a node above this node
dlang.mixin(params, inherited);
}
// Get list of attributes explicitly listed in the markup
var attributes;
if(has("dom-attributes-explicit")){
// Standard path to get list of user specified attributes
attributes = node.attributes;
}else if(has("dom-attributes-specified-flag")){
// Special processing needed for IE8, to skip a few faux values in attributes[]
attributes = darray.filter(node.attributes, function(a){
return a.specified;
});
}else{
// Special path for IE6-7, avoid (sometimes >100) bogus entries in node.attributes
var clone = /^input$|^img$/i.test(node.nodeName) ? node : node.cloneNode(false),
attrs = clone.outerHTML.replace(/=[^\s"']+|="[^"]*"|='[^']*'/g, "").replace(/^\s*<[a-zA-Z0-9]*\s*/, "").replace(/\s*>.*$/, "");
attributes = darray.map(attrs.split(/\s+/), function(name){
var lcName = name.toLowerCase();
return {
name: name,
// getAttribute() doesn't work for button.value, returns innerHTML of button.
// but getAttributeNode().value doesn't work for the form.encType or li.value
value: (node.nodeName == "LI" && name == "value") || lcName == "enctype" ?
node.getAttribute(lcName) : node.getAttributeNode(lcName).value
};
});
}
// Hash to convert scoped attribute name (ex: data-dojo17-params) to something friendly (ex: data-dojo-params)
// TODO: remove scope for 2.0
var scope = options.scope || dojo._scopeName,
attrData = "data-" + scope + "-", // typically "data-dojo-"
hash = {};
if(scope !== "dojo"){
hash[attrData + "props"] = "data-dojo-props";
hash[attrData + "type"] = "data-dojo-type";
hash[attrData + "mixins"] = "data-dojo-mixins";
hash[scope + "type"] = "dojoType";
hash[attrData + "id"] = "data-dojo-id";
}
// Read in attributes and process them, including data-dojo-props, data-dojo-type,
// dojoAttachPoint, etc., as well as normal foo=bar attributes.
var i = 0, item, funcAttrs = [], jsname, extra;
while(item = attributes[i++]){
var name = item.name,
lcName = name.toLowerCase(),
value = item.value;
switch(hash[lcName] || lcName){
// Already processed, just ignore
case "data-dojo-type":
case "dojotype":
case "data-dojo-mixins":
break;
// Data-dojo-props. Save for later to make sure it overrides direct foo=bar settings
case "data-dojo-props":
extra = value;
break;
// data-dojo-id or jsId. TODO: drop jsId in 2.0
case "data-dojo-id":
case "jsid":
jsname = value;
break;
// For the benefit of _Templated
case "data-dojo-attach-point":
case "dojoattachpoint":
params.dojoAttachPoint = value;
break;
case "data-dojo-attach-event":
case "dojoattachevent":
params.dojoAttachEvent = value;
break;
// Special parameter handling needed for IE
case "class":
params["class"] = node.className;
break;
case "style":
params["style"] = node.style && node.style.cssText;
break;
default:
// Normal attribute, ex: value="123"
// Find attribute in widget corresponding to specified name.
// May involve case conversion, ex: onclick --> onClick
if(!(name in proto)){
var map = getNameMap(ctor);
name = map[lcName] || name;
}
// Set params[name] to value, doing type conversion
if(name in proto){
switch(typeof proto[name]){
case "string":
params[name] = value;
break;
case "number":
params[name] = value.length ? Number(value) : NaN;
break;
case "boolean":
// for checked/disabled value might be "" or "checked". interpret as true.
params[name] = value.toLowerCase() != "false";
break;
case "function":
if(value === "" || value.search(/[^\w\.]+/i) != -1){
// The user has specified some text for a function like "return x+5"
params[name] = new Function(value);
}else{
// The user has specified the name of a global function like "myOnClick"
// or a single word function "return"
params[name] = dlang.getObject(value, false) || new Function(value);
}
funcAttrs.push(name); // prevent "double connect", see #15026
break;
default:
var pVal = proto[name];
params[name] =
(pVal && "length" in pVal) ? (value ? value.split(/\s*,\s*/) : []) : // array
(pVal instanceof Date) ?
(value == "" ? new Date("") : // the NaN of dates
value == "now" ? new Date() : // current date
dates.fromISOString(value)) :
(pVal instanceof _Url) ? (dojo.baseUrl + value) :
myEval(value);
}
}else{
params[name] = value;
}
}
}
// Remove function attributes from DOMNode to prevent "double connect" problem, see #15026.
// Do this as a separate loop since attributes[] is often a live collection (depends on the browser though).
for(var j = 0; j < funcAttrs.length; j++){
var lcfname = funcAttrs[j].toLowerCase();
node.removeAttribute(lcfname);
node[lcfname] = null;
}
// Mix things found in data-dojo-props into the params, overriding any direct settings
if(extra){
try{
extra = myEval.call(options.propsThis, "{" + extra + "}");
dlang.mixin(params, extra);
}catch(e){
// give the user a pointer to their invalid parameters. FIXME: can we kill this in production?
throw new Error(e.toString() + " in data-dojo-props='" + extra + "'");
}
}
// Any parameters specified in "mixin" override everything else.
dlang.mixin(params, mixin);
// Get <script> nodes associated with this widget, if they weren't specified explicitly
if(!scripts){
scripts = (ctor && (ctor._noScript || proto._noScript) ? [] : query("> script[type^='dojo/']", node));
}
// Process <script type="dojo/*"> script tags
// <script type="dojo/method" data-dojo-event="foo"> tags are added to params, and passed to
// the widget on instantiation.
// <script type="dojo/method"> tags (with no event) are executed after instantiation
// <script type="dojo/connect" data-dojo-event="foo"> tags are dojo.connected after instantiation,
// and likewise with <script type="dojo/aspect" data-dojo-method="foo">
// <script type="dojo/watch" data-dojo-prop="foo"> tags are dojo.watch after instantiation
// <script type="dojo/on" data-dojo-event="foo"> tags are dojo.on after instantiation
// note: dojo/* script tags cannot exist in self closing widgets, like <input />
var aspects = [], // aspects to connect after instantiation
calls = [], // functions to call after instantiation
watches = [], // functions to watch after instantiation
ons = []; // functions to on after instantiation
if(scripts){
for(i = 0; i < scripts.length; i++){
var script = scripts[i];
node.removeChild(script);
// FIXME: drop event="" support in 2.0. use data-dojo-event="" instead
var event = (script.getAttribute(attrData + "event") || script.getAttribute("event")),
prop = script.getAttribute(attrData + "prop"),
method = script.getAttribute(attrData + "method"),
advice = script.getAttribute(attrData + "advice"),
scriptType = script.getAttribute("type"),
nf = this._functionFromScript(script, attrData);
if(event){
if(scriptType == "dojo/connect"){
aspects.push({ method: event, func: nf });
}else if(scriptType == "dojo/on"){
ons.push({ event: event, func: nf });
}else{
// <script type="dojo/method" data-dojo-event="foo">
// TODO for 2.0: use data-dojo-method="foo" instead (also affects dijit/Declaration)
params[event] = nf;
}
}else if(scriptType == "dojo/aspect"){
aspects.push({ method: method, advice: advice, func: nf });
}else if(scriptType == "dojo/watch"){
watches.push({ prop: prop, func: nf });
}else{
calls.push(nf);
}
}
}
// create the instance
var markupFactory = ctor.markupFactory || proto.markupFactory;
var instance = markupFactory ? markupFactory(params, node, ctor) : new ctor(params, node);
function onInstantiate(instance){
// map it to the JS namespace if that makes sense
if(jsname){
dlang.setObject(jsname, instance);
}
// process connections and startup functions
for(i = 0; i < aspects.length; i++){
aspect[aspects[i].advice || "after"](instance, aspects[i].method, dlang.hitch(instance, aspects[i].func), true);
}
for(i = 0; i < calls.length; i++){
calls[i].call(instance);
}
for(i = 0; i < watches.length; i++){
instance.watch(watches[i].prop, watches[i].func);
}
for(i = 0; i < ons.length; i++){
don(instance, ons[i].event, ons[i].func);
}
return instance;
}
if(instance.then){
return instance.then(onInstantiate);
}else{
return onInstantiate(instance);
}
},
scan: function(root, options){
// summary:
// Scan a DOM tree and return an array of objects representing the DOMNodes
// that need to be turned into widgets.
// description:
// Search specified node (or document root node) recursively for class instances
// and return an array of objects that represent potential widgets to be
// instantiated. Searches for either data-dojo-type="MID" or dojoType="MID" where
// "MID" is a module ID like "dijit/form/Button" or a fully qualified Class name
// like "dijit/form/Button". If the MID is not currently available, scan will
// attempt to require() in the module.
//
// See parser.parse() for details of markup.
// root: DomNode?
// A default starting root node from which to start the parsing. Can be
// omitted, defaulting to the entire document. If omitted, the `options`
// object can be passed in this place. If the `options` object has a
// `rootNode` member, that is used.
// options: Object
// a kwArgs options object, see parse() for details
//
// returns: Promise
// A promise that is resolved with the nodes that have been parsed.
var list = [], // Output List
mids = [], // An array of modules that are not yet loaded
midsHash = {}; // Used to keep the mids array unique
var dojoType = (options.scope || dojo._scopeName) + "Type", // typically "dojoType"
attrData = "data-" + (options.scope || dojo._scopeName) + "-", // typically "data-dojo-"
dataDojoType = attrData + "type", // typically "data-dojo-type"
dataDojoTextDir = attrData + "textdir", // typically "data-dojo-textdir"
dataDojoMixins = attrData + "mixins"; // typically "data-dojo-mixins"
// Info on DOMNode currently being processed
var node = root.firstChild;
// Info on parent of DOMNode currently being processed
// - inherited: dir, lang, and textDir setting of parent, or inherited by parent
// - parent: pointer to identical structure for my parent (or null if no parent)
// - scripts: if specified, collects <script type="dojo/..."> type nodes from children
var inherited = options.inherited;
if(!inherited){
function findAncestorAttr(node, attr){
return (node.getAttribute && node.getAttribute(attr)) ||
(node.parentNode && findAncestorAttr(node.parentNode, attr));
}
inherited = {
dir: findAncestorAttr(root, "dir"),
lang: findAncestorAttr(root, "lang"),
textDir: findAncestorAttr(root, dataDojoTextDir)
};
for(var key in inherited){
if(!inherited[key]){
delete inherited[key];
}
}
}
// Metadata about parent node
var parent = {
inherited: inherited
};
// For collecting <script type="dojo/..."> type nodes (when null, we don't need to collect)
var scripts;
// when true, only look for <script type="dojo/..."> tags, and don't recurse to children
var scriptsOnly;
function getEffective(parent){
// summary:
// Get effective dir, lang, textDir settings for specified obj
// (matching "parent" object structure above), and do caching.
// Take care not to return null entries.
if(!parent.inherited){
parent.inherited = {};
var node = parent.node,
grandparent = getEffective(parent.parent);
var inherited = {
dir: node.getAttribute("dir") || grandparent.dir,
lang: node.getAttribute("lang") || grandparent.lang,
textDir: node.getAttribute(dataDojoTextDir) || grandparent.textDir
};
for(var key in inherited){
if(inherited[key]){
parent.inherited[key] = inherited[key];
}
}
}
return parent.inherited;
}
// DFS on DOM tree, collecting nodes with data-dojo-type specified.
while(true){
if(!node){
// Finished this level, continue to parent's next sibling
if(!parent || !parent.node){
break;
}
node = parent.node.nextSibling;
scriptsOnly = false;
parent = parent.parent;
scripts = parent.scripts;
continue;
}
if(node.nodeType != 1){
// Text or comment node, skip to next sibling
node = node.nextSibling;
continue;
}
if(scripts && node.nodeName.toLowerCase() == "script"){
// Save <script type="dojo/..."> for parent, then continue to next sibling
type = node.getAttribute("type");
if(type && /^dojo\/\w/i.test(type)){
scripts.push(node);
}
node = node.nextSibling;
continue;
}
if(scriptsOnly){
// scriptsOnly flag is set, we have already collected scripts if the parent wants them, so now we shouldn't
// continue further analysis of the node and will continue to the next sibling
node = node.nextSibling;
continue;
}
// Check for data-dojo-type attribute, fallback to backward compatible dojoType
// TODO: Remove dojoType in 2.0
var type = node.getAttribute(dataDojoType) || node.getAttribute(dojoType);
// Short circuit for leaf nodes containing nothing [but text]
var firstChild = node.firstChild;
if(!type && (!firstChild || (firstChild.nodeType == 3 && !firstChild.nextSibling))){
node = node.nextSibling;
continue;
}
// Meta data about current node
var current;
var ctor = null;
if(type){
// If dojoType/data-dojo-type specified, add to output array of nodes to instantiate.
var mixinsValue = node.getAttribute(dataDojoMixins),
types = mixinsValue ? [type].concat(mixinsValue.split(/\s*,\s*/)) : [type];
// Note: won't find classes declared via dojo/Declaration or any modules that haven't been
// loaded yet so use try/catch to avoid throw from require()
try{
ctor = getCtor(types, options.contextRequire);
}catch(e){}
// If the constructor was not found, check to see if it has modules that can be loaded
if(!ctor){
darray.forEach(types, function(t){
if(~t.indexOf('/') && !midsHash[t]){
// If the type looks like a MID and it currently isn't in the array of MIDs to load, add it.
midsHash[t] = true;
mids[mids.length] = t;
}
});
}
var childScripts = ctor && !ctor.prototype._noScript ? [] : null; // <script> nodes that are parent's children
// Setup meta data about this widget node, and save it to list of nodes to instantiate
current = {
types: types,
ctor: ctor,
parent: parent,
node: node,
scripts: childScripts
};
current.inherited = getEffective(current); // dir & lang settings for current node, explicit or inherited
list.push(current);
}else{
// Meta data about this non-widget node
current = {
node: node,
scripts: scripts,
parent: parent
};
}
// Recurse, collecting <script type="dojo/..."> children, and also looking for
// descendant nodes with dojoType specified (unless the widget has the stopParser flag).
// When finished with children, go to my next sibling.
scripts = childScripts;
scriptsOnly = node.stopParser || (ctor && ctor.prototype.stopParser && !(options.template));
parent = current;
node = firstChild;
}
var d = new Deferred();
// If there are modules to load then require them in
if(mids.length){
// Warn that there are modules being auto-required
if(has("dojo-debug-messages")){
console.warn("WARNING: Modules being Auto-Required: " + mids.join(", "));
}
var r = options.contextRequire || require;
r(mids, function(){
// Go through list of widget nodes, filling in missing constructors, and filtering out nodes that shouldn't
// be instantiated due to a stopParser flag on an ancestor that we belatedly learned about due to
// auto-require of a module like ContentPane. Assumes list is in DFS order.
d.resolve(darray.filter(list, function(widget){
if(!widget.ctor){
// Attempt to find the constructor again. Still won't find classes defined via
// dijit/Declaration so need to try/catch.
try{
widget.ctor = getCtor(widget.types, options.contextRequire);
}catch(e){}
}
// Get the parent widget
var parent = widget.parent;
while(parent && !parent.types){
parent = parent.parent;
}
// Return false if this node should be skipped due to stopParser on an ancestor.
// Since list[] is in DFS order, this loop will always set parent.instantiateChildren before
// trying to compute widget.instantiate.
var proto = widget.ctor && widget.ctor.prototype;
widget.instantiateChildren = !(proto && proto.stopParser && !(options.template));
widget.instantiate = !parent || (parent.instantiate && parent.instantiateChildren);
return widget.instantiate;
}));
});
}else{
// There were no modules to load, so just resolve with the parsed nodes. This separate code path is for
// efficiency, to avoid running the require() and the callback code above.
d.resolve(list);
}
// Return the promise
return d.promise;
},
_require: function(/*DOMNode*/ script, /*Object?*/ options){
// summary:
// Helper for _scanAMD(). Takes a `<script type=dojo/require>bar: "acme/bar", ...</script>` node,
// calls require() to load the specified modules and (asynchronously) assign them to the specified global
// variables, and returns a Promise for when that operation completes.
//
// In the example above, it is effectively doing a require(["acme/bar", ...], function(a){ bar = a; }).
var hash = myEval("{" + script.innerHTML + "}"), // can't use dojo/json::parse() because maybe no quotes
vars = [],
mids = [],
d = new Deferred();
var contextRequire = (options && options.contextRequire) || require;
for(var name in hash){
vars.push(name);
mids.push(hash[name]);
}
contextRequire(mids, function(){
for(var i = 0; i < vars.length; i++){
dlang.setObject(vars[i], arguments[i]);
}
d.resolve(arguments);
});
return d.promise;
},
_scanAmd: function(root, options){
// summary:
// Scans the DOM for any declarative requires and returns their values.
// description:
// Looks for `<script type=dojo/require>bar: "acme/bar", ...</script>` node, calls require() to load the
// specified modules and (asynchronously) assign them to the specified global variables,
// and returns a Promise for when those operations complete.
// root: DomNode
// The node to base the scan from.
// options: Object?
// a kwArgs options object, see parse() for details
// Promise that resolves when all the <script type=dojo/require> nodes have finished loading.
var deferred = new Deferred(),
promise = deferred.promise;
deferred.resolve(true);
var self = this;
query("script[type='dojo/require']", root).forEach(function(node){
// Fire off require() call for specified modules. Chain this require to fire after
// any previous requires complete, so that layers can be loaded before individual module require()'s fire.
promise = promise.then(function(){
return self._require(node, options);
});
// Remove from DOM so it isn't seen again
node.parentNode.removeChild(node);
});
return promise;
},
parse: function(rootNode, options){
// summary:
// Scan the DOM for class instances, and instantiate them.
// description:
// Search specified node (or root node) recursively for class instances,
// and instantiate them. Searches for either data-dojo-type="Class" or
// dojoType="Class" where "Class" is a a fully qualified class name,
// like `dijit/form/Button`
//
// Using `data-dojo-type`:
// Attributes using can be mixed into the parameters used to instantiate the
// Class by using a `data-dojo-props` attribute on the node being converted.
// `data-dojo-props` should be a string attribute to be converted from JSON.
//
// Using `dojoType`:
// Attributes are read from the original domNode and converted to appropriate
// types by looking up the Class prototype values. This is the default behavior
// from Dojo 1.0 to Dojo 1.5. `dojoType` support is deprecated, and will
// go away in Dojo 2.0.
// rootNode: DomNode?
// A default starting root node from which to start the parsing. Can be
// omitted, defaulting to the entire document. If omitted, the `options`
// object can be passed in this place. If the `options` object has a
// `rootNode` member, that is used.
// options: Object?
// A hash of options.
//
// - noStart: Boolean?:
// when set will prevent the parser from calling .startup()
// when locating the nodes.
// - rootNode: DomNode?:
// identical to the function's `rootNode` argument, though
// allowed to be passed in via this `options object.
// - template: Boolean:
// If true, ignores ContentPane's stopParser flag and parses contents inside of
// a ContentPane inside of a template. This allows dojoAttachPoint on widgets/nodes
// nested inside the ContentPane to work.
// - inherited: Object:
// Hash possibly containing dir and lang settings to be applied to
// parsed widgets, unless there's another setting on a sub-node that overrides
// - scope: String:
// Root for attribute names to search for. If scopeName is dojo,
// will search for data-dojo-type (or dojoType). For backwards compatibility
// reasons defaults to dojo._scopeName (which is "dojo" except when
// multi-version support is used, when it will be something like dojo16, dojo20, etc.)
// - propsThis: Object:
// If specified, "this" referenced from data-dojo-props will refer to propsThis.
// Intended for use from the widgets-in-template feature of `dijit._WidgetsInTemplateMixin`
// - contextRequire: Function:
// If specified, this require is utilised for looking resolving modules instead of the
// `dojo/parser` context `require()`. Intended for use from the widgets-in-template feature of
// `dijit._WidgetsInTemplateMixin`.
// returns: Mixed
// Returns a blended object that is an array of the instantiated objects, but also can include
// a promise that is resolved with the instantiated objects. This is done for backwards
// compatibility. If the parser auto-requires modules, it will always behave in a promise
// fashion and `parser.parse().then(function(instances){...})` should be used.
// example:
// Parse all widgets on a page:
// | parser.parse();
// example:
// Parse all classes within the node with id="foo"
// | parser.parse(dojo.byId('foo'));
// example:
// Parse all classes in a page, but do not call .startup() on any
// child
// | parser.parse({ noStart: true })
// example:
// Parse all classes in a node, but do not call .startup()
// | parser.parse(someNode, { noStart:true });
// | // or
// | parser.parse({ noStart:true, rootNode: someNode });
// determine the root node and options based on the passed arguments.
var root;
if(!options && rootNode && rootNode.rootNode){
options = rootNode;
root = options.rootNode;
}else if(rootNode && dlang.isObject(rootNode) && !("nodeType" in rootNode)){
options = rootNode;
}else{
root = rootNode;
}
root = root ? dom.byId(root) : dwindow.body();
options = options || {};
var mixin = options.template ? { template: true } : {},
instances = [],
self = this;
// First scan for any <script type=dojo/require> nodes, and execute.
// Then scan for all nodes with data-dojo-type, and load any unloaded modules.
// Then build the object instances. Add instances to already existing (but empty) instances[] array,
// which may already have been returned to caller. Also, use otherwise to collect and throw any errors
// that occur during the parse().
var p =
this._scanAmd(root, options).then(function(){
return self.scan(root, options);
}).then(function(parsedNodes){
return self._instantiate(parsedNodes, mixin, options, true);
}).then(function(_instances){
// Copy the instances into the instances[] array we declared above, and are accessing as
// our return value.
return instances = instances.concat(_instances);
}).otherwise(function(e){
// TODO Modify to follow better pattern for promise error management when available
console.error("dojo/parser::parse() error", e);
throw e;
});
// Blend the array with the promise
dlang.mixin(instances, p);
return instances;
}
};
if(has("extend-dojo")){
dojo.parser = parser;
}
// Register the parser callback. It should be the first callback
// after the a11y test.
if(config.parseOnLoad){
ready(100, parser, "parse");
}
return parser;
});