-
Notifications
You must be signed in to change notification settings - Fork 2
/
tectonic.js
1432 lines (1361 loc) · 55.7 KB
/
tectonic.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
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// (c) 2015-2016 Corin Lawson
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
//
(function bootstrap (root, factory) {
// AMD. Register as an anonymous module.
if (typeof define === 'function' && define.amd) {
define([], factory);
}
// Node. Does not work with strict CommonJS, but only CommonJS-like
// environments that support module.exports, like Node.
else if (typeof module === 'object' && module.exports) {
module.exports = factory();
}
// Browser globals (root is window)
else {
root.Tectonic = factory();
}
}(this, function factory () {
'use strict';
// Introduction
// ============
//
// Tectonic is a functional rendering engine for DOM nodes, heavily inspired
// by [Beebole's PURE](https://github.com/pure/pure) rendering engine. It
// ascribes to PURE's unobtusive philosophy, whereby HTML code is completely
// free of any application logic or new syntax and JavaScript code is
// uninhibited by presentational concerns. This is achieved by both PURE and
// Tectonic by the use of a directive object that marries HTML referenced by
// CSS selectors to properties in your application's data. Where Tectonic
// departs from PURE is in the use of functions, known as renderers, to
// directly manipulate DOM nodes. This permits Tectonic to provide two-way
// data flow via a `parse` method, which makes use of an inverse function that
// is attached to a renderer. Tectonic takes the stance that it is your
// responsiblity to provide a context to any function that you provide. I.e.
// Tectonic won't be using `call` or `apply` on functions that you define,
// consider using [underscore](http://underscorejs.org/)'s `bind` method or
// simply defining your methods inside a closure.
//
// The directive object's keys are used to find elements and/or element
// attributes in the DOM. That element's content or its attribute is then
// updated according to the directive's value for that key. The directive's
// value can specify a literal string, a property in a given data object, or a
// combination of both. It can also duplicate the element and recursively
// render those elements for each item of an array in the given data object.
// Constructor
// ===========
// A Tectonic object wraps the specified `element` and defines methods for
// compile, render, parse, etc. An optional `basis` may also be specified;
// think of the basis as an untouched version of `element`, which will be used
// by Tectonic as a point of reference.
var Tectonic = function Tectonic (element, basis) {
// `element` is expected to be a DOM `Node`, otherwise we assume it's a
// different Tectonic object and get its element. Note this also works well
// with jQuery objects.
if (!(element instanceof Node)) {
element = element.get(0);
}
// The most common use case is to wrap an element that's already in the
// browser's DOM before any DOM changes, hence the element is currently
// *untouched* so the basis is simply a clone of the element.
if (arguments.length < 2) {
basis = element.cloneNode(true);
}
// Accessor for this object's `element`. Think `jQuery#get`.
this.get = function get () {
return element;
};
// Accessor for this object's context.
// The specified context object is bound to the compiled renderer function.
var ctx = this;
this.context = function context (context) {
if (arguments.length === 0) {
return ctx;
}
ctx = context;
return this;
};
this.clone = function clone (deep) {
// Creates a deep copy of this object. Note that returned object's
// `element` will be detached from the browser's DOM.
if (deep) {
return new Tectonic(element.cloneNode(true), basis.cloneNode(true));
}
// Creates a shallow copy of this object.
return new Tectonic(element, basis);
};
// Compares the specified `other` object to this object. When a single
// argument is provided, returns `true` if and only if `other` is an
// instance of Tectonic and both its `element` and `basis` are identical to
// this object's `element` and `basis`, respectively. When two arguments
// are specified, returns `true` if and only if `other` and `otherBasis` are
// identical to this object's `element` and `basis`, respectively.
this.equals = function equals (other, otherBasis) {
switch (arguments.length) {
case 2:
return element === other && basis === otherBasis;
case 1:
if (other instanceof Tectonic) {
return this === other || other.equals(element, basis);
}
default:
return false;
}
};
// Like compile but also inspects this object's `element`'s class names for
// additional directives.
var autoCompile = function autoCompile (data, directive) {
return Tectonic.plugin.autoCompile(element, directive || {}, data);
};
// Updates this object's `element` to reflect the specified `data` according
// to the specified `directive`. Returns this object for chaining.
// The `directive` may be an object (to be compiled) or a pre-compiled
// renderer function.
this.render = function reader (data, directive) {
// Behave like `autoRender` when there's no directive.
if (!directive) {
directive = autoCompile(data);
}
// Accept a pre-compiled renderer (or any function really!), otherwise
// create a renderer function by compiling the `directive`.
if (typeof directive !== 'function') {
directive = this.compile(directive);
}
// Execute the renderer!
// Although the common use case is for the renderer to simply make changes
// directly to `element`, we are nevertheless prepared for the renderer to
// create a new element.
var newElement = directive.call(this, data);
if (newElement !== element) {
// If it is the case that a new element was created then replace this
// object's `element` with the `newElement`.
if (element.parentNode) {
element.parentNode.replaceChild(newElement, element);
}
element = newElement;
}
// Return this object to support chaining.
return this;
};
// Creates a data object from this object's `element` according to the
// specified `directive`. Returns an object that contains the data that
// would be required to `render` this object, that would result in this
// object's `element`. Note that this can be called before `render`,
// consider using `parse` to extract default values from your browser's DOM.
this.parse = function parse (directive) {
// There's no autoParse here...
if (!directive) {
throw new Error("Directive missing.");
}
// Accept a pre-compiled renderer, otherwise create a renderer function by
// compiling the `directive`.
if (typeof directive !== 'function') {
directive = this.compile(directive);
}
// Parse is only posible because [`compile`](#compilers) attaches an
// `inverse` function to the renderer function.
return directive.inverse.call(this);
};
// Create a renderer and its inverse function, according to the specified
// `directive`. Returns a function that can be used in place of a directive
// to `render` and `parse`.
this.compile = function compile (directive) {
// The real work is done by [`Tectonic.plugin.compile`](#compilers).
var renderer = Tectonic.plugin.compile([basis], directive);
// The resulting function is curried with this object's `element` or a
// clone of `element` if it is called within a different context. For
// example you can produce many nodes like so
//
// var render = new Tectonic(element).compile(directive);
// for (var i in models) {
// document.body.appendChild(render(models[i]));
// }
var tectonic = this;
var bounded = function bounded (data) {
return renderer.call(ctx, data,
tectonic.equals(this)
? element
: basis.cloneNode(true));
};
bounded.inverse = function inverse (el) {
return renderer.inverse.call(ctx,
(tectonic.equals(this)
? element
: el) ||
basis, {});
};
return bounded;
};
// Almost exactly like `render`, except this object's `element`'s class
// names are inspected for additional directives.
this.autoRender = function autoRender (data, directive) {
return this.render(data, autoCompile(data, directive));
};
};
// Selectors
// =========
// Creates an object that specifies what part of the DOM should be
// manipulated, given the specified key from a directive. The directive's key
// is broken into four optional parts.
var parseSelector = function parseSelector (str) {
var spec = { raw: str };
var match = str.match(
/^ *([^@]*?)?? *(@([^ ]+?))? *(:(before|after|toggle))? *$/);
if (!match) {
throw new Error("invalid selector: '" + str + "'");
}
// The first part specifies a (CSS) selector used to find the element to be
// updated.
spec.selector = match[1];
// The next part, if present, must be preceeded by an `@`, and names the
// attribute of the element to be updated.
spec.attr = match[3];
// The last part, is like a pseudo-class selector, but in this case it
// signifies that the content should be prepended (before) or appended
// (after). Note that it doesn't make sense for tectonic is manipulate
// pseudo-classes.
spec.prepend = match[5] === 'before';
spec.append = match[5] === 'after';
// A special pseudo-class, just for Tectonic, the that signifies that the
// content must be switched between two alternative values.
spec.toggle = match[5] === 'toggle';
return spec;
};
// Compiler
// ========
// Creates a renderer function for one key-value pair (i.e. the specified
// `spec` and `template` pair). Note that the specified `basis` is an array of
// DOM nodes to support recursively calling [`compile`](#compilers) with the
// results [`Tectonic.plugin.find`](#section-79) as the `basis` (e.g. see
// [`Tectonic.plugin.loopWriter`](#section-91)). This method is responsible
// for coordinating the five methods that is generated from the
// [`Tectonic.plugin`](#plugin), namely a finder, writer, formatter, parser
// and reader. To that end, no assumption is made about the information that
// these five plugin methods need, therefore they are called with all the
// information that we have.
var compiler = function compiler (basis, spec, template) {
// The real work of finding the correct element to update is done by the
// function returned from [`Tectonic.plugin.finder`](#finders). The
// resulting `find` function mostly likely doesn't need `template` but
// finder does use it to despatch on its type. Likewise, `basis` is unlikely
// to be used except in the event that the element in the DOM has already
// been removed by a previous renderer. The important peice of information
// that finder needs is `spec.selector`.
var find = Tectonic.plugin.finder(basis, spec, template);
// The real work of updating the DOM is done by the function returned from
// [`Tectonic.plugin.writer`](#writers). Similar to finder, writer is
// unlikely to need `basis` or `template`. The important information that
// writer needs is `spec.attr`, `spec.append`, `spec.prepend`, etc.
var write = Tectonic.plugin.writer(basis, spec, template);
// The real work of finding the content to place into the DOM is done by the
// function returned from [`Tectonic.plugin.formatter`](#formatters).
// Unlike finder and writer, formatter is unlikely to need `spec` or
// `basis`. The important information is almost exclusively contained with
// in template.
var format = Tectonic.plugin.formatter(basis, spec, template);
// The real work of extracting data back out of the DOM is done by the
// function returned from [`Tectonic.plugin.parser`](#parsers). It is the
// converse of write, and uses the same information contained in `spec`.
var parse = Tectonic.plugin.parser(basis, spec, template);
// The real work of putting data back into a data object is done by the
// function returned from [`Tectonic.plugin.reader`](#readers). It is the
// converse of format, and as such the important information is almost
// exclusively contained with in template.
var read = Tectonic.plugin.reader(basis, spec, template);
// These renderers take a signgle `target` element to be rendered with
// `data`. The other arguments are optional and typically only present when
// handling loop directives.
var renderAction = function renderAction (data, target) {
// The same data will be applied to every node selected by `spec`.
// The common use case will be a single node for the data.
var bindData = format.apply(this, arguments);
var nodes = find(target, bindData, basis);
for (var i = 0, ii = nodes.length; i < ii; i++) {
var boundData = bindData;
// Data can be tailored to each and every node by supplying a function
// that returns a function as the value of the directive. E.g. see
// [`Tectonic.toggleClass`](#section-179).
if (typeof boundData === 'function') {
boundData = bindData.call(this, data, nodes[i], i, nodes);
}
// Now, update the DOM.
var newNode = write.call(this, nodes[i], boundData, i, nodes);
// Typically `write` simply updates the DOM, but if a different node is
// produced, update the DOM with that node instead.
if (newNode !== nodes[i]) {
if (newNode && nodes[i].parentNode) {
nodes[i].parentNode.replaceChild(newNode, nodes[i]);
}
nodes[i] = newNode;
}
}
};
// Parsing relies on `parse` and `read`
if (parse && read) {
renderAction.inverse = function inverse (source, data) {
if (read.length > 2) {
return read.call(this, data, parse(source, find), find(source));
}
return read.call(this, data, parse(source, find));
};
}
return renderAction;
};
// Plugin
// ======
// Used by [`Tectonic.plugin.formatter`](#formatters) and
// [`Tectonic.plugin.reader`](#readers) to break a template string into
// literal strings and paths of a property in a data object.
var stringDataPattern = / *(?:"([^"]*)"|'([^']*)'|([^'" ]+)) */g;
var identity = function identity (a) {
return a;
};
// Most of the core functionality of Tectonic is exposed here, in order to
// allow other authors to extend the functionality. For example, authors
// working with SVG could override [`Tectonic.plugin.writer`](#writers) and
// [`Tectonic.plugin.parser`](#parsers) (or some of its helpers, such as
// `attrWriter` and `loopParser`); authors using Backbone models could
// override `Tectonic.plugin.reader` and
// [`Tectonic.plugin.formatter`](#formatters) (or just `propFormatter` and
// `propReader`); or if you wish to use jQuery/Sizzle,
// [`Tectonic.plugin.find`](#section-79) needs to be overridden.
//
// The five plugin methods used in [`compiler`](#compiler) above, act as
// despatchers based on either the spec or the template.
Tectonic.plugin = {
// Finders
// -------
// Returns a function to retrieve an array of Nodes that need to be updated.
// The returned function's parameters must be a DOM `Node` and data object.
// The returned function's return value must be an array of nodes.
finder: function finder (basis, spec, template) {
if (!spec.selector || /^ *\.? *$/.test(spec.selector)) {
return this.topFinder(basis, spec, template);
} else if (typeof template === 'object' && !this.isArray(template)) {
return this.loopFinder(basis, spec, template);
} else {
return this.queryFinder(basis, spec, template);
}
},
// Used in the case of when the directive refers to the current element
// being rendered. All the selectors in the following directive will result
// in calling this method.
// {
// '': 'empty string',
// '@attr': 'also works with attributes',
// '.': 'dot',
// '.@attr': 'as you would expect'
// }
topFinder: function topFinder () {
return function topFinder (target) {
return [target];
};
},
// Used in the case of looping directives.
// Additionally, this method also ensures that there is the correct number
// of elements, one for each item in the loop.
loopFinder: function loopFinder (basis, spec) {
var p = this;
return function loopFinder (target, data, basis) {
var length = data && data.length || 0;
var ownerDocument = target.ownerDocument || document;
var nodes = p.find([target], spec.selector);
var i, ii = nodes.length;
// When there is no data...
if (!length) {
// ...remove all the nodes in the DOM.
if (ii) {
// First, remove all but one node.
for (i = 1; i < ii; i++) {
nodes[i].parentNode.removeChild(nodes[i]);
}
if (ownerDocument) {
// Replace the last node with a marker so that we may insert new
// nodes in the future
nodes[0].parentNode.replaceChild(
ownerDocument.createComment(spec.raw), nodes[0]);
} else {
// Otherwise remove the last node if a valid document cannot be
// found.
nodes[0].parentNode.removeChild(nodes[0]);
}
}
// No data means no nodes.
return [];
}
// When the number of nodes matches the number of data items. There's
// nothing more to do.
else if (length === ii) {
return nodes;
}
// Otherwise, add or remove nodes as needed.
else {
// Newly created nodes are sourced from the original DOM element. This
// permits easy striping behaviour.
var newNodes = p.find(basis, spec.selector);
var sentinal, lastNode, mod = newNodes.length;
// When no nodes are found in the DOM we must search for the specail
// marker that is placed into the DOM when there is no data. Here we
// perform a breadth-first search starting with `target` and exiting
// when the comment matching the directive's key is found.
if (!ii) {
lastNode = newNodes[0].cloneNode(true);
var q = [target];
while (q.length) {
var comment = q.pop();
if (comment.nodeType === 8 && comment.nodeValue === spec.raw) {
comment.parentNode.replaceChild(lastNode, comment);
q.length = 0;
} else if (comment.childNodes.length) {
q.splice.apply(q, [0, 0].concat(
Array.prototype.slice.call(comment.childNodes)));
}
}
ii = 1;
nodes = [lastNode];
}
// Otherwise, take note of the lastNode so that we may insert more
// nodes as needed.
if (length !== ii) {
lastNode = nodes[ii - 1];
// Insertion will be either before the element after the last
// matching node or (because `lastNode` is infact the last node of
// it's parent) insertion will be achieved by appending to the
// parent node.
var insert;
sentinal = lastNode.nextSibling;
lastNode = lastNode.parentNode;
if (lastNode) {
insert = sentinal ? function insert (newNode) {
lastNode.insertBefore(newNode, sentinal);
nodes.push(newNode);
} : function insert (newNode) {
lastNode.appendChild(newNode);
nodes.push(newNode);
};
}
// Alternatively, there is no parent, hence nowhere to insert.
else {
insert = function insert (newNode) {
nodes.push(newNode);
};
}
// Continue to insert nodes while the number of nodes is less then
// the number of items in the data.
for (i = Math.min(ii, length); i < length; i++) {
insert(newNodes[i % mod].cloneNode(true));
}
// Alternatively, remove nodes until the number of nodes matches the
// number of items in the data.
for (; i < ii; i++) {
lastNode.removeChild(nodes[i]);
}
// And remove nodes from the returned array as necessary.
nodes.length = length;
}
}
return nodes;
};
},
// Used in the default case, passes `spec.selector` to
// [`Tectonic.plugin.find`](#section-79).
queryFinder: function queryFinder (basis, spec) {
var p = this;
return function queryFinder (target) {
return p.find([target], spec.selector);
};
},
// Finds elements within the specified array of Nodes, `contexts`, matching
// the specified `selector`. Returns an array of Nodes.
//
// The default implementation uses `querySelectorAll` and this method is
// used by various other plugin methods. This is the best point for a
// jQuery/Sizzle (or similar) plugin to introduce its own functionality.
//
// It's important to return a real array (not a `NodeList`) for use in other
// plugin methods (e.g. `loopFinder`).
find: function find (contexts, selector) {
var elements = [];
var found, i, ii, j, jj;
for (i = 0, ii = contexts.length; i < ii; i++) {
found = contexts[i].querySelectorAll(selector);
for (j = 0, jj = found.length; j < jj; j++) {
elements.push(found[j]);
}
}
return elements;
},
// Writers
// -------
// Returns a function to update the DOM for a given `spec`.
// The returned function's parameters must be a `target` DOM `Node` and data
// object. The returned function's return value must be the DOM `Node`
// written to, if this node is not the `target` then the `target` will be
// replaced with the returned node.
writer: function writer (basis, spec, template) {
if (spec.attr) {
return this.attrWriter(basis, spec, template);
} else if (typeof template === 'object' && !this.isArray(template)) {
return this.loopWriter(basis, spec, template);
} else {
return this.elementWriter(basis, spec, template);
}
},
// Used in the case where `spec` refers to an element's attribute.
attrWriter: function attrWriter (basis, spec) {
return function attrWriter (target, value) {
// Attribute selectors are particularly useful for elements.
if (target.nodeType === 1) {
var tagName = target.tagName.toUpperCase();
// As a convenience, handle dropdown boxes specially.
if (tagName === "OPTION" &&
spec.attr === "selected") {
var selected = value === 'false' ? false : Boolean(value);
target.selected = selected;
if (selected) {
target.setAttribute("selected", value);
} else {
target.removeAttribute("selected");
}
}
// Check and radio boxes also require specail handling.
else if (tagName === "INPUT" &&
spec.attr === "checked") {
var checked = value === 'false' ? false : Boolean(value);
target.checked = checked;
if (checked) {
target.setAttribute("checked", value);
} else {
target.removeAttribute("checked");
}
}
// Disabled elements are special too.
else if (spec.attr === "disabled" &&
/^(INPUT|TEXTAREA|BUTTON|SELECT|OPTION|OPTGROUP|FIELDSET)$/.test(
tagName)) {
var disabled = value === 'false' ? false : Boolean(value);
target.disabled = disabled;
if (disabled) {
target.setAttribute("disabled", value);
} else {
target.removeAttribute("disabled");
}
}
// Treat class attribute (and aliases) specially.
else if (spec.attr === "class" ||
spec.attr === "className" ||
spec.attr === "classList") {
if (spec.toggle) {
var classList = ' ' + (target.getAttribute('class') || '') + ' ';
if (classList.indexOf(' ' + value + ' ') >= 0) {
classList = classList.replace(' ' + value + ' ', ' ');
} else {
classList += value;
}
value = classList.replace(/^ +| +$/g, '');
} else if (spec.append) {
value = target.getAttribute('class') + ' ' + value;
} else if (spec.prepend) {
value = value + ' ' + target.getAttribute('class');
}
target.setAttribute('class', value);
}
// Otherwise, use `setAttribute` with `spec.attr` as-is and support
// append and prepend.
else {
if (spec.append) {
value = target.getAttribute(spec.attr) + value;
} else if (spec.prepend) {
value = value + target.getAttribute(spec.attr);
}
target.setAttribute(spec.attr, value);
}
}
// Attribute selectors might also be useful for other node types, but
// since other node types do not have a `setAttribute` method then we'll
// just assign values directly to properties of the `target` node.
else {
if (spec.append) {
value = target[spec.attr] + value;
} else if (spec.prepend) {
value = value + target[spec.attr];
}
target[spec.attr] = value;
}
return target;
};
},
// Used in the case of looping directives.
loopWriter: function loopWriter (basis, spec, template) {
// Find the key that contains the loop spec (e.g. `'<-'`).
var loopSpec = this.parseLoopSpec(template);
// Recursively call [`compile`](#compilers) where a subtree of `basis`
// becomes the new `basis` and the object referenced by `loopSpec` becomes
// the new directive.
var renderer = this.compile(
this.find(basis, spec.selector),
loopSpec.directive);
return function loopWriter (target, items, i, targets) {
// An item of the array becomes the data for the recursive call.
var data = items[i];
// The lefthand side of `<-`, if present, is used to refer to the data.
if (loopSpec.lhs) {
(data = {})[loopSpec.lhs] = items[i];
}
return renderer.call(this, data, target, i, targets, items);
};
},
// Used in the case where `spec` refers to an element.
elementWriter: function elementWriter (basis, spec) {
return function elementWriter (target, value) {
// When `target` and `value` are the same, there's nothing to do.
if (target !== value) {
// Particularly useful for elements.
if (target.nodeType === 1) {
// `value` will probably need to be appended, hence we need a node.
var valueNode = value;
if (!(value instanceof Node)) {
valueNode = document.createTextNode(value);
}
// Since input elements do not allow child nodes it is more useful
// to treat the `value` property as its content. This behaviour also
// works well for textareas.
if (value !== valueNode &&
(target.tagName.toUpperCase() === 'INPUT' ||
target.tagName.toUpperCase() === 'TEXTAREA')) {
if (spec.append) {
value = target.value + value;
} else if (spec.prepend) {
value = value + target.value;
}
target.value = value;
}
// Appending is straightforward and happens to be equivalent to
// prepend when `target` is already empty.
else if (spec.append || spec.prepend && !target.childNodes.length) {
target.appendChild(valueNode);
}
// Prepend is now straightforward since `target` is not empty.
else if (spec.prepend) {
target.insertBefore(valueNode, target.childNodes[0]);
}
// When `value` is text, hold back from replacing the entire node.
else if (value !== valueNode) {
if (target.childNodes.length) {
target.innerHTML = "";
}
target.appendChild(valueNode);
}
// Otherwise, let the renderer complete the replacement.
// See [`renderAction`](#section-47).
else {
target = value;
}
}
// To extend the functionality to other node types, simply use
// `nodeValue`.
else if (!(value instanceof Node)) {
if (spec.append) {
value = target.nodeValue + value;
} else if (spec.prepend) {
value = value + target.nodeValue;
}
target.nodeValue = value;
}
// Otherwise, let the renderer complete the replacement.
// See [`renderAction`](#section-47).
else {
target = value;
}
}
return target;
};
},
// Formatters
// ----------
// Returns a function to find, extract and prepare, from a data object, the
// content to be written into the DOM.
// The returned function's parameters must be the data object and the target
// DOM `Node`.
// The returned function's return value can be either a string that will be
// set as the content of the target element or attribute or an element, in
// which case the target node will be replaced.
formatter: function formatter (basis, spec, template) {
var found, parts;
switch (typeof template) {
case 'function':
return template;
case 'object':
if (this.isArray(template)) {
return this.propFormatter(basis, spec, template);
} else {
return this.loopFormatter(basis, spec, template);
}
// Strings specify either a path of a property in the data object, or a
// literal string or both. Literal strings can be surrounded by either
// single or double quotes.
case 'string':
parts = [];
while ((found = stringDataPattern.exec(template))) {
if (found[3]) {
parts.push(this.propFormatter(basis, spec, found[3].split('.')));
} else {
parts.push(this.stringFormatter(basis, spec, found[1] || found[2]));
}
}
if (parts.length === 1) {
return parts[0];
}
else if (parts.length > 1) {
return this.concatenator(parts);
} else {
return this.emptyFormatter();
}
default:
return this.emptyFormatter();
}
},
// Used in the case where the data should be treated as a literal string.
stringFormatter: function stringFormatter (basis, spec, literal) {
return function stringFormatter () {
return String(literal);
};
},
// Used for directives that do not specify either a function, object, data
// property, nor literal string.
emptyFormatter: function emptyFormatter () {
return function emptyFormatter () {
return '';
};
},
// Used for directives that specify a property in a data object. The `path`
// argument must be an array of strings.
propFormatter: function propFormatter (basis, spec, path) {
return function propFormatter (data) {
// Stop following the path as soon as data is a false-y.
for (var i = 0, ii = path.length; i < ii && data; i++) {
// Treat empty string in `path` as no-op.
if (path[i]) {
data = data[path[i]];
}
}
return data;
};
},
// Used in the case of looping directives.
loopFormatter: function loopFormatter (basis, spec, template) {
// The righthand side specifies the property in the data that is the
// array.
var formatter = this.propFormatter(
basis,
spec,
this.parseLoopSpec(template).rhs.split('.'));
// The array will be used without further processing unless sort or filter
// is specified.
if (!('sort' in template) && !template.filter) {
return formatter;
}
return function loopFormatter () {
var filtered;
var array = formatter.apply(this, arguments);
if (template.filter) {
filtered = [];
for (var i = 0, ii = array.length; i < ii; i++) {
// Execute the `filter` function bound to the loop spec, providing
// it with the item, index and array.
if (template.filter(array[i], i, array)) {
filtered.push(array[i]);
}
}
} else {
// Do not attempt to sort the original array.
filtered = Array.prototype.slice.call(array);
}
if ('sort' in template) {
filtered.sort(template.sort);
}
return filtered;
};
},
// Returns a function that executes each function of `parts` in turn and
// concatenates each, returning the result.
concatenator: function concatenator (parts) {
return function concatenator () {
var i, ii, part, cat = "";
for (i = 0, ii = parts.length; i < ii; i++) {
part = parts[i].apply(this, arguments);
if (typeof part !== 'undefined') {
cat += part;
}
}
return cat;
};
},
// Parsers
// -------
// Returns a function to recontruct, from the DOM, the value to be placed in
// a data object.
// The returned function's parameters must be a `source` node from the DOM
// and a `finder` function (such as a function returned by
// [`Tectonic.plugin.finder`](#finders)). The `finder` function may be used
// to find nodes within `source` or `basis`.
// The returned function's return value must be the value to be placed into
// a reconstructed data object.
parser: function parser (basis, spec, template) {
if (spec.attr) {
return this.attrParser(basis, spec, template);
} else if (typeof template === 'object' && !this.isArray(template)) {
return this.loopParser(basis, spec, template);
} else {
return this.elementParser(basis, spec, template);
}
},
// Used in the case of looping directives.
loopParser: function loopParser (basis, spec, template) {
// Find the key that contains the loop spec (e.g. `'<-'`).
var loopSpec = this.parseLoopSpec(template);
// Recursively call [`Tectonic.plugin.compile`](#compilers) where a
// subtree of `basis` becomes the new `basis` and the object referenced by
// `loopSpec` becomes the new directive.
var renderer = this.compile(
this.find(basis, spec.selector),
loopSpec.directive);
var p = this;
return function loopParser (source) {
// Each node in the DOM corresponds to one item in the returned array.
var nodes = p.find([source], spec.selector);
var array = [], data;
for (var i = 0, ii = nodes.length; i < ii; i++) {
// Recontruct the data from the DOM node.
data = renderer.inverse.call(this, nodes[i], data = {});
// The lefthand side of `<-`, if present, is used to refer to the
// data.
if (loopSpec.lhs) {
data = data[loopSpec.lhs];
}
array[i] = data;
}
return array;
};
},
// Used in the case where `spec` refers to an element.
elementParser: function elementParser (basis, spec) {
var p = this;
return function elementParser (source, finder) {
var value, original;
// Use the first found element; we assume they are all the same (if they
// are different consider using a looping directive).
var target = finder(source)[0];
// When there's no target then we can't go any further.
if (!target) {
return;
}
// When appending or prepending, also find the same element in the basis
// in order to compare later.
if (spec.append || spec.prepend) {
original = finder(basis[0])[0];
}
// Particularly useful for elements.
if (target.nodeType === 1) {
// Since input elements do not allow child nodes it is more useful to
// treat the `value` property as its content. This behaviour also
// works well for textareas.
if (target.tagName.toUpperCase() === 'INPUT' ||
target.tagName.toUpperCase() === 'TEXTAREA') {
value = target.value;
// Given that we earlier found the same element from the `basis`,
// compare `value` to the original value and find the difference.
if (original) {
if (target.tagName.toUpperCase() === 'INPUT') {
value = p.diff(
value,
original.getAttribute('value'),
spec.append);
} else {
value = p.diff(value, original.textContent, spec.append);
}
}
}
// Otherwise `textContent` is considered to be the data.
else {
value = target.textContent;
if (original) {
value = p.diff(value, original.textContent, spec.append);
}
}
}
// To extend the functionality to other node types, simply use
// `nodeValue`.
else {
value = target.nodeValue;
if (original) {
value = p.diff(value, original.nodeValue, spec.append);
}
}
return value;
};
},
// Used in the case where `spec` refers to an element's attribute.
attrParser: function attrParser (basis, spec) {
var p = this;
return function attrParser (source, finder) {
var value, original;
// Use the first found element; we assume they are all the same (if they
// are different consider using a looping directive).
var target = finder(source)[0];
// When there's no target then we can't go any further.
if (!target) {
return;
}
// When appending or prepending, also find the same element in the basis
// in order to compare later.
if (spec.append || spec.prepend) {
original = finder(basis[0])[0];
}
// Attribute selectors are particularly useful for elements.
if (target.nodeType === 1) {
var tagName = target.tagName.toUpperCase();
// Both `selected`, `disabled` and `checked` attributes are booleans.
if (tagName === "OPTION" &&
spec.attr === "selected") {
value = target.selected;
} else if (tagName === "INPUT" &&
spec.attr === "checked") {
value = target.checked;
} else if (spec.attr === "disabled" &&
/^(INPUT|TEXTAREA|BUTTON|SELECT|OPTION|OPTGROUP|FIELDSET)$/.test(
tagName)) {
value = target.disabled;