-
Notifications
You must be signed in to change notification settings - Fork 47
/
transclude.js
2209 lines (1841 loc) · 80.8 KB
/
transclude.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
/* author: Said Achmiz */
/* license: MIT */
/****************/
/* TRANSCLUSION */
/****************/
/* Transclusion is dynamic insertion, into a document, of part or all of
a different document.
I. BASICS
=========
Put an include-link into the page, and at load time, the link will be
replaced by the content it specifies.
An include-link is a link (<a> tag) which has the `include` class, e.g.:
<a class="include" href="/Sidenotes#comparisons"></a>
At load time, this tag will be replaced with the `#comparisons` section of
the /Sidenotes page.
If the include-link’s URL (i.e., the value of its `href` attribute) has no
hash (a.k.a. fragment identifier), then the entire page content will be
transcluded. (If the page contains an element with the `markdownBody` ID,
then only the contents of that element will be transcluded; otherwise, the
contents of the `body` element will be transcluded; if neither element is
present, then the complete contents of the page will be transcluded.)
If the include-link’s URL has a hash, and the page content contains an
element with an ID matching the hash, then only that element (or that
element’s contents; see the `include-unwrap` option, below) will be
transcluded. (If the URL has a hash but the hash does not identify any
element contained in the page content, nothing is transcluded.)
(See the ADVANCED section, below, for other ways to use an include-link’s
URL hash to specify parts of a page to transclude.)
II. OPTIONS
===========
Several optional classes modify the behavior of include-links:
include-annotation
include-content
If the include-link is an annotated link, then instead of transcluding
the linked content, the annotation for the linked content may be
transcluded.
The default behavior is set via the
Transclude.transcludeAnnotationsByDefault property. If this is set to
`true`, then fully (not partially!) annotated links transclude the
annotation unless the `include-content` class is set (in which case they
transclude their linked content). If it is set to `false`, then fully
annotated links transclude the annotation only if the
`include-annotation` class is set (otherwise they transclude their
linked content).
Note that merely partially annotated links always default to
transcluding content, unless the `include-annotation` class is set.
(See also the `include-annotation-partial` alias class.)
include-strict
By default, include-links are lazy-loaded. A lazy-loaded include-link
will not trigger (i.e., transclude its content) immediately at load
time. Instead, it will wait until the user scrolls down to the part of
the page where the link is located, or pops up a popup that contains
that part of the page, or otherwise “looks” at the include-link’s
surrounding context. Only then will the transclusion take place.
A strict include-link, on the other hand, triggers immediately at
load time.
Note that `include-strict` implies `include-even-when-collapsed`,
because otherwise odd behavior can result (eg. a ‘strict’ transclusion
in the first line or two of a collapse will be visibly untranscluded;
and collapses blocking strict transclusion can lead to unpredictable
breakage when the contents of the transclusion are depended upon by the
rest of the page, and collapses are added/removed by editors).
include-lazy
By default, include-links are loaded when they are within some scroll
distance away from the view rect of their scroll container (i.e., the
viewport, or the frame of a pop-frame, etc.); this is done so that the
transcluded content is likely to already be loaded by the time the user
scrolls to the include-link’s position in the document flow.
The `include-lazy` option makes the transclusion behavior lazier than
usual; an include-link with this class will trigger only when it crosses
the boundary of the viewport (or the scroll container’s view rect).
Note that if the `include-strict` option is set, then `include-lazy`
will have no effect. Similarly, if the `include-even-when-collapsed`
option is *not* set (assuming that `include-strict` is also not set),
then `include-lazy` will have no effect if the include-link is within
a collapsed block.
include-even-when-collapsed
Normally, an include-link that is inside a collapsed block will not
trigger at load time; instead, it will trigger only when it is revealed
by expansion of its containing collapse block(s). The
`include-even-when-collapsed` class disables this delay, forcing the
include-link to trigger when revealed by scrolling (if it is not marked
as `include-strict`; otherwise, `include-strict` will force the
include-link to trigger at load time, regardless of anything to do with
collapses) even if, at such time, it is within a collapsed block.
Note that the `include-strict` and `include-even-when-collapsed` options
do not do the same thing; the former implies the latter, but not the
other way around.
include-unwrap
Normally, when an include-link’s URL specifies an element ID to
transclude, the element with that ID is transcluded in its entirety.
When the `include-unwrap` option is used, the element itself is
discarded, and only the element’s contents are transcluded.
(This option has no effect unless the include-link’s URL hash specifies
a single element ID to transclude.)
include-block-context
data-block-context-options
Normally, when an include-link’s URL specifies an element ID to
transclude, only (at most; see `include-unwrap`) that element is
transcluded. When the `include-block-context` option is used, not only
the identified element itself, but also its containing block element
(and everything within) will be included. (What “block element” means
in this context is not the same as what the HTML spec means by the
term. Determination of what counts as a block element is done in a
content-aware way.)
If `include-unwrap` is used as well as `include-block-context`, then the
identified element’s containing block will be unwrapped, and the
included content will be all the child nodes of the identified element’s
containing block.
(This option has no effect unless the include-link’s URL hash specifies
a single element ID to transclude.)
The `data-block-context-options` attribute allows various options to be
specified for how block context should be determined and handled. The
value of this attribute is a pipe (`|`) separated list of option fields.
The following options may be specified:
expanded
Expanded block context mode omits paragraphs (the <p> element) from
consideration as containing blocks.
include-replace-container
data-replace-container-selector
data-replace-container-options
Normally, when transclusion occurs, the transcluded content replaces the
include-link in the page, leaving any surrounding elements untouched.
When the `include-replace-container` option is used, the include-link’s
parent element, instead of just the include-link itself, is replaced by
the transcluded content. (This means that any other contents of the
include-link’s parent element are also discarded.) If the include-link
has no parent element (if, for example, it is an immediate child node of
a DocumentFragment), then it will simply behave normally (as if the
`include-replace-container option was not set at all).
The `data-replace-container-selector` attribute allows specification of
a list of CSS selectors that should be used to locate the ‘container’
element to be replaced by the transcluded content (rather than simply
using the include-link’s immediate parent). The value of this attribute
is a pipe (`|`) separated list of CSS selectors. Each selector is
checked in turn; if no containing element matching the given selector is
found, the next selector is checked, and so on. If no containing element
matching any of the given selectors is found, then the include-link will
default to replacing its immediate parent element (as if the
`data-replace-container-selector` attribute were absent). (But see the
`strict` option of the `data-replace-container-options` attribute,
below).
The `data-replace-container-options` attribute allows various options to
be specified for how container replacement should proceed. The value of
this attribute is a pipe (`|`) separated list of option fields. The
following options may be specified:
strict
If this option is specified, and a value is provided for the
`data-replace-container-selector` attribute, and no containing
element matching any of the specified selectors is found, then the
include-link will *not* default to replacing its immediate parent
(as is the default behavior), but will instead replace nothing
(i.e., it will behave as if the `include-replace-selector` option
were not specified at all).
include-rectify-not
Normally, when transclusion occurs, the surrounding HTML structure is
intelligently rectified, to preserve block containment rules and so on.
When the `include-rectify-not` option is used, this rectification is
not done.
(Not currently used on gwern.net.)
include-identify-not
Normally, if the include-link has a nonempty ‘id’ attribute, and that
ID does not occur in the transcluded content (after any unwrapping; see
‘include-unwrap’, above, for details), the content will be wrapped in a
DIV element, which will be given the ID of the include-link. When the
`include-identify-not` option is used, this will not be done.
(Not currently used on gwern.net.)
include-localize-not
When content specified by an include-link is transcluded into the base
page, and the transcluded content has headings, should those headings be
added to the page’s table of contents? When transcluded content has
footnote references, should those citations be integrated into the host
page’s footnote numbering, and should the associated footnotes be added
to the host page’s footnotes section?
Normally, the answer (and it’s the same answer for both questions, and
several related ones such as link qualification) is determined on the
basis of the content type of the transcluded content, the context in
which it’s being transcluded (e.g., a backlink context block), and some
other factors. If the `include-localize-not` option is used, however,
the content will NOT be “localized”, no matter what other conditions
may obtain.
include-spinner
include-spinner-not
Shows or hides the “loading spinner” that is shown at the site of the
include-link while content to be transcluded is being retrieved. In the
absence of either of these classes, the spinner will be shown or not,
depending on context. Using either class causes the spinner to be shown
or not shown (respectively), unconditionally.
(Note that these two classes, unlike the others listed in this section,
DO NOT mark a link as an include-link. They must be used in conjunction
with the `include` class, or with one or more of the optional include
classes listed here.)
III. ADVANCED
=============
1. Transclude range syntax
--------------------------
The transclusion feature supports PmWiki-style transclude range syntax,
very similar to the one described here:
https://www.pmwiki.org/wiki/PmWiki/IncludeOtherPages#includeanchor
To use transclude range syntax, an include-link’s URL should have a “double”
hash, i.e. a hash consisting of two ‘#’-prefixed parts:
<a class="include" href="/Sidenotes#tufte-css#tables"></a>
This will include all parts of the "/Sidenotes" page’s content starting from
the element with ID `tufte-css`, all the way up to (but *not* including!)
the element with ID `tables`.
Either the first or the second identifier (the parts after the ‘#’) may
instead be empty. The possibilities are:
#foo#bar
Include everything starting from element `#foo` up to (but not
including) element `#bar`.
##bar
Include everything from the start of the page content up to (but not
including) element `#bar`.
#foo#
Include everything starting from element `#foo` to the end of the page.
##
Include the entire page content (same as not having a hash at all).
In all cases, only the page content is considered, not any “page furniture”
(i.e., only the contents of `#markdownBody`, if present; or only the
contents of `<body>`, if present; or the whole page, otherwise).
If an element of one of the specified IDs is not found in the page, the
transclusion fails.
If both elements are present, but the end element does not follow the start
element in the page order (i.e., if the start element comes after the end
element, or if they are the same), then the transcluded content is empty.
2. Include template
-------------------
The `data-include-template` attribute allows selection of include template
to use.
(Note that some include data sources specify a template by default;
the `data-include-template` attribute overrides the default in such cases.)
If a template is specified, the included content is treated as a template
data source, rather than being included directly. (See comment for the
templateDataFromHTML() function for information about how template data
is specified in HTML. Note that some data sources provide template data in
pre-constructed object form, which bypasses the need to extract it from
HTML source.)
If the value of this attribute begins with the ‘$’ character, then the rest
if the attribute value (after the dollar sign) is treated as a key into the
template data object, rather than directly as the name of a template file.
This allows a template data source to specify different templates for use
in different contexts. (For example, a template data source may specify a
default template, to be used when transcluding normally, and a different
template to be used when the transcluded content is to be used as the
content of a pop-frame. In such a case, the template data object might have
a field with key `popFrameTemplate` whose value is the name of a template,
and the include-link’s `data-include-template` attribute would have a value
of `$popFrameTemplate`.)
3. Selector-based inclusion/exclusion
-------------------------------------
The `data-include-selector` and `data-include-selector-not` attributes allow
the use of CSS selectors to specify parts of the included DOM subtree to
include or omit. (If both attributes are present,
`data-include-selector-not` is applied first.)
The `data-include-selector-options`, `data-include-selector-not-options`,
and `data-include-selector-general-options` attributes allows various
options to be specified for how the selectors should be applied. The values
of these attributes are pipe (`|`) separated lists of option fields. The
`-options` version of the attribute applies only to `data-include-selector`;
`-not-options` applies only to `data-include-selector-not`; and
`-general-options` applies to both. (The specific options attributes take
precedence over the general options attribute.)
The following options may be specified:
first
Select only the first element matching the specified selector, instead
of selecting all matching elements. (In other words, use querySelector()
instead of querySelectorAll().)
(NOTE: `data-include-selector` may be seen as a generalization of the
`include-block-context` option, described above. Note, however, that both
`include-block-context` and either or both of `data-include-selector` /
`data-include-selector-not` may be used simultaneously. The effects of the
data attributes are applied last, after all `include-*` options have been
applied.)
IV. ALIASES
===========
The following classes, set on include-links, function as aliases for various
combinations of the above-described functionality. Each entry below lists
the alias class (or set of multiple specific classes, in some cases),
followed by the combination of classes, data attributes, etc. to which the
alias is equivalent. Some entries also include usage notes.
class="include-block-context-expanded"
class="include-block-context"
data-block-context-options="expanded"
“Expanded block context” typically means “broaden the block context
beyond a single paragraph”.
class="include-annotation-partial"
class="include-annotation"
data-include-selector-not=".annotation-abstract, .file-includes, figure, .data-field-separator"
data-template-fields="annotationClassSuffix:$"
data-annotation-class-suffix="-partial"
Includes only the metadata of annotations (omitting the annotation
abstract, i.e. the body of the annotation, if any). Formats the included
annotation as a partial.
class="include-annotation-core"
class="include-annotation"
data-include-selector=".annotation-abstract, .file-includes"
Essentially the opposite of .include-annotation-partial; includes only
the annotation abstract, omitting metadata. (If there is no abstract -
i.e., if the annotation is a partial - the included content will be
empty.)
class="include-content-core"
class="include-content"
data-include-selector-not="#footnotes, #backlinks-section,
#similars-section, #link-bibliography-section,
#page-metadata .link-tags, #page-metadata .page-metadata-fields"
Include a page’s content, omitting “auxiliary” content sections
(Footnotes, Further Reading, Backlinks, Link Bibliography), as well as
the page tags and the date/status/confidence/importance/etc. metadata
fields block.
class="include-content-no-header"
class="include-unwrap"
data-include-selector-not="h1, h2, h3, h4, h5, h6"
data-include-selector-not-options="first"
Applied to an include-link that targets a <section>, will include only
the content of the section; the <section> will be unwrapped, and the
heading discarded. (If applied in some other case, behavior may be
unpredictable.)
class="include-caption-not"
data-include-selector-not=".caption-wrapper"
Normally, media (image, video, audio) include-links which have
annotations will, when transcluded, get a <figcaption> whose contents
are the abstract of the annotation. If the `include-caption-not` class
is set, the caption is omitted. (This class has no effect if applied to
include-links of non-media content types.)
*/
/******************************************************************************/
/* Extract template data from an HTML string or DOM object by looking for
elements with either the `data-template-field` or the
`data-template-fields` attribute.
If the `data-template-fields` attribute is not present but the
`data-template-field` attribute is present, then the value of the latter
attribute is treated as the data field name; the .innerHTML of the
element is the field value.
If the `data-template-fields` attribute is present, then the attribute
value is treated as a comma-separated list of
`fieldName:fieldValueIdentifier` pairs. For each pair, the part before the
colon (the fieldName) is the data field name. The part after the colon
(the fieldValueIdentifier) can be interpreted in one of two ways:
If the fieldValueIdentifier begins with a dollar sign (the ‘$’ character),
then the rest of the identifier (after the dollar sign) is treated as the
name of the attribute of the given element which holds the field value.
If the fieldValueIdentifier is _only_ the ‘$’ character, then the field
value will be the value of the data attribute that corresponds to the
field name (i.e., if the field is `fooBar`, then the field value will be
taken from attribute `data-foo-bar`).
If the fieldValueIdentifier begins with a period (the ‘.’ character), then
the rest of the identifier (after the period) is treated as the name of the
DOM object property of the given element which holds the field value.
If the fieldValueIdentifier is _only_ the ‘.’ character, then the field
value will be the value of the element property matching the field name
(i.e., if the field name is `fooBar`, then the field value will be the
value of the element’s .fooBar property).
Examples:
<span data-template-field="foo">Bar</span>
This element defines a data field with name `foo` and value `Bar`.
<span data-template-fields="foo:$title" title="Bar"></span>
This element defines one data field, with name `foo` and value `Bar`.
<span data-template-fields="foo:$title, bar:.tagName" title="Baz"></span>
This element defines two data fields: one with name `foo` and value
`Baz`,and one with name `bar` and value `SPAN`.
<span data-template-field="foo:title" title="Bar"></span>
This element defines no data fields. (Likely this is a typo, and
the desired attribute name is actually `data-template-fields`; note
the plural form.)
*/
// (string|Document|DocumentFragment|Element) => object
function templateDataFromHTML(html) {
let dataObject = { };
if (( html instanceof Document
|| html instanceof DocumentFragment) == false)
html = newDocument(html);
html.querySelectorAll("[data-template-field], [data-template-fields]").forEach(element => {
if (element.dataset.templateFields) {
element.dataset.templateFields.split(",").forEach(templateField => {
let [ beforeColon, afterColon ] = templateField.trim().split(":");
let fieldName = beforeColon.trim();
let fieldValueIdentifier = afterColon.trim();
if (fieldValueIdentifier.startsWith(".")) {
dataObject[fieldName] = fieldValueIdentifier == "."
? element[fieldName]
: element[fieldValueIdentifier.slice(1)];
} else if (fieldValueIdentifier.startsWith("$")) {
dataObject[fieldName] = fieldValueIdentifier == "$"
? element.dataset[fieldName]
: element.getAttribute(fieldValueIdentifier.slice(1));
}
});
} else {
dataObject[element.dataset.templateField] = element.innerHTML;
}
});
return dataObject;
}
/************************************************************************/
/* Return either true or false, having evaluated the template expression
(used in conditionals, e.g. `<[IF !foo & bar]>baz<[IFEND]>`).
*/
function evaluateTemplateExpression(expr, valueFunction = (() => null)) {
if (expr == "_TRUE_")
return true;
if (expr == "_FALSE_")
return false;
return evaluateTemplateExpression(expr.replace(
// Quotes.
/(['"])(.*?)(\1)/g,
(match, leftQuote, quotedExpr, rightQuote) =>
"<<" + fixedEncodeURIComponent(quotedExpr) + ">>"
).replace(
// Brackets.
/\s*\[\s*(.+?)\s*\]\s*/g,
(match, bracketedExpr) =>
(evaluateTemplateExpression(bracketedExpr, valueFunction)
? "_TRUE_"
: "_FALSE_")
).replace(
// Boolean AND, OR.
/\s*([^&|]+?)\s*([&|])\s*(.+)\s*/g,
(match, leftOperand, operator, rightOperand) => {
let leftOperandTrue = evaluateTemplateExpression(leftOperand, valueFunction);
let rightOperandTrue = evaluateTemplateExpression(rightOperand, valueFunction);
let expressionTrue = operator == "&"
? (leftOperandTrue && rightOperandTrue)
: (leftOperandTrue || rightOperandTrue);
return expressionTrue ? "_TRUE_" : "_FALSE_";
}
).replace(
// Boolean NOT.
/\s*!\s*(\S+)\s*/g,
(match, operand) =>
(evaluateTemplateExpression(operand, valueFunction)
? "_FALSE_"
: "_TRUE_")
).replace(
// Comparison.
/\s*(\S+)\s+(\S+)\s*/,
(match, leftOperand, rightOperand) => {
let constantRegExp = new RegExp(/^_(\S*)_$/);
if ( constantRegExp.test(leftOperand)
|| constantRegExp.test(rightOperand)) {
return ( evaluateTemplateExpression(leftOperand, valueFunction)
== evaluateTemplateExpression(rightOperand, valueFunction)
? "_TRUE_"
: "_FALSE_");
} else {
let literalRegExp = new RegExp(/^<<(.*)>>$/);
leftOperand = literalRegExp.test(leftOperand)
? decodeURIComponent(leftOperand.slice(2, -2))
: valueFunction(leftOperand);
rightOperand = literalRegExp.test(rightOperand)
? decodeURIComponent(rightOperand.slice(2, -2))
: valueFunction(rightOperand);
return (leftOperand == rightOperand
? "_TRUE_"
: "_FALSE_");
}
}
).replace(/\s*(\S+)\s*/g,
// Constant or field name.
(match, constantOrFieldName) =>
(/^_(\S*)_$/.test(constantOrFieldName)
? constantOrFieldName
: (valueFunction(constantOrFieldName)
? "_TRUE_"
: "_FALSE_"))
));
}
/******************************************************************************/
/* Fill a template with provided reference data (supplemented by an optional
context object).
Reference data may be a data object, or else an HTML string (in which case
the templateDataFromHTML() function is used to extract data from the HTML).
If no ‘data’ argument is provided, then the template itself will be parsed
to extract reference data (again, using the templateDataFromHTML()
function).
(Context argument must be an object, not a string.)
Available options (defaults):
preserveSurroundingWhitespaceInConditionals (false)
If true, `<[IF foo]> bar <[IFEND]>` becomes ` bar `;
if false, `bar`.
fireContentLoadEvent (false)
If true, a GW.contentDidLoad event is fired on the filled template.
*/
// (string, string|object, object, object) => DocumentFragment
function fillTemplate(template, data = null, context = null, options = { }) {
if ( template == null
|| template == "LOADING_FAILED")
return null;
// If no data source is provided, use the template itself as data source.
if ( data == null
|| data == "LOADING_FAILED")
data = template;
/* If the data source is a string, assume it to be HTML and extract data;
likewise, if the data source is a DocumentFragment, extract data.
*/
if ( typeof data == "string"
|| data instanceof DocumentFragment)
data = templateDataFromHTML(data);
/* Data variables specified in the provided context argument (if any)
take precedence over the reference data.
*/
let valueFunction = (fieldName) => {
return (context && context[fieldName]
? context[fieldName]
: (data ? data[fieldName] : null));
};
// Line continuations.
template = template.replace(
/>\\\n\s*</gs,
(match) => "><"
);
// Comments.
template = template.replace(
/<\(.+?\)>/gs,
(match) => ""
);
// Escapes.
template = template.replace(
/\\(.)/gs,
(match, escaped) => "<[:" + escaped.codePointAt(0) + ":]>"
);
/* Conditionals. JavaScript’s regexps do not support recursion, so we
keep running the replacement until no conditionals remain.
*/
let didReplace;
do {
didReplace = false;
template = template.replace(
/<\[IF([0-9]*)\s+(.+?)\]>(.+?)(?:<\[ELSE\1\]>(.+?))?<\[IF\1END\]>/gs,
(match, nestLevel, expr, ifValue, elseValue) => {
didReplace = true;
let returnValue = evaluateTemplateExpression(expr, valueFunction)
? (ifValue ?? "")
: (elseValue ?? "");
return options.preserveSurroundingWhitespaceInConditionals
? returnValue
: returnValue.trim();
});
} while (didReplace);
// Data variable substitution.
template = template.replace(
/<\{(.+?)\}>/g,
(match, fieldName) => (valueFunction(fieldName) ?? "")
);
// Escapes, redux.
template = template.replace(
/<\[:(.+?):\]>/gs,
(match, codePointSequence) => String.fromCodePoint(...(codePointSequence.split("/").map(x => parseInt(x))))
);
// Construct DOM tree from filled template.
let outputDocument = newDocument(template);
// Fire GW.contentDidLoad event, if need be.
if (options.fireContentLoadEvent) {
let loadEventInfo = {
container: outputDocument,
document: outputDocument
};
if (options.loadEventInfo)
for (let [key, value] of Object.entries(options.loadEventInfo))
if ([ "container", "document" ].includes(key) == false)
loadEventInfo[key] = value;
GW.notificationCenter.fireEvent("GW.contentDidLoad", loadEventInfo);
}
return outputDocument;
}
/*****************************************************************************/
/* Construct synthetic include-link. The optional ‘link’ argument may be
a string, a URL object, or an HTMLAnchorElement, in which case it, or its
.href property, is used as the ‘href’ attribute of the synthesized
include-link.
*/
function synthesizeIncludeLink(link, attributes, properties) {
let includeLink = newElement("A", attributes, properties);
if (link == null)
return includeLink;
if (typeof link == "string") {
includeLink.href = link;
} else if (link instanceof HTMLAnchorElement) {
includeLink.href = link.getAttribute("href");
} else if (link instanceof URL) {
includeLink.href = link.href;
} else {
return null;
}
if (link instanceof HTMLAnchorElement) {
// Import certain data attributes.
[ "linkContentType",
"backlinkTargetUrl",
"urlArchive",
"urlHtml"
].forEach(dataAttributeName => {
if (link.dataset[dataAttributeName])
includeLink.dataset[dataAttributeName] = link.dataset[dataAttributeName];
});
// Import certain link classes.
/* See corresponding note in annotations.js.
—SA 2024-02-16
*/
[ "link-live",
"link-page",
"link-dropcap",
"link-annotated",
"link-annotated-partial",
"content-transform-not"
].forEach(targetClass => {
if (link.classList.contains(targetClass))
includeLink.classList.add(targetClass);
});
}
// In case no include classes have been added yet...
if (Transclude.isIncludeLink(includeLink) == false)
includeLink.classList.add("include");
return includeLink;
}
/*************************************************************************/
/* Return appropriate loadLocation for given include-link. (May be null.)
*/
function loadLocationForIncludeLink(includeLink) {
if (Transclude.isAnnotationTransclude(includeLink) == false) {
return ( Content.sourceURLsForLink(includeLink)?.first
?? includeLink.eventInfo.loadLocation);
} else {
return null;
}
}
/*******************************************************************************/
/* Return appropriate contentType string for given include-link. (May be null.)
*/
function contentTypeIdentifierForIncludeLink(includeLink) {
let contentType = null;
if ( Transclude.isAnnotationTransclude(includeLink)
|| ( Content.contentTypes.localFragment.matches(includeLink)
&& /^\/metadata\/annotation\/[^\/]+$/.test(includeLink.pathname))) {
contentType = "annotation";
} else {
let referenceData = Transclude.dataProviderForLink(includeLink).referenceDataForLink(includeLink);
if ( referenceData
&& referenceData.contentTypeClass != null)
contentType = referenceData.contentTypeClass.replace(/([a-z])-([a-z])/g, (match, p1, p2) => (p1 + p2.toUpperCase()));
}
return contentType;
}
/*****************************************************************/
/* Standardized parsing for a pipe (`|`) separated options field.
(Returns null if no non-whitespace options are provided.)
*/
function parsePipedOptions(attributeValue) {
return attributeValue?.split("|").map(x => x.trim()).filter(x => x > "");
}
/******************************************************************************/
/* Returns true if content specified by the given include-link should be
“localized” (i.e., integrated into the page structure - footnotes, table of
contents, etc. - of the document into which it is being transcluded); false
otherwise.
*/
function shouldLocalizeContentFromLink(includeLink) {
if (includeLink.classList.contains("include-localize-not"))
return false;
if (includeLink.eventInfo.localize == false)
return false;
if (Transclude.dataProviderForLink(includeLink).shouldLocalizeContentFromLink?.(includeLink) == false)
return false;
return true;
}
/*******************************************************************************/
/* Adds `block-context-highlighted` class to element targeted by the given link
in the given document, if the targeted element exists, and if it is NOT the
only immediately child of the document itself.
*/
function highlightTargetElementInDocument(link, doc) {
let targetElement = targetElementInDocument(link, doc);
if (targetElement
&& ( targetElement.parentNode == doc
&& isOnlyChild(targetElement)
) == false) {
targetElement.classList.add("block-context-highlighted");
/* When highlighting <div> elements, place the manicule appropriately
(and only if appropriate).
*/
if ( targetElement.tagName == "DIV"
&& previousBlockOf(targetElement)?.matches(".heading") == false)
targetElement.querySelector("p")?.classList.add("block-context-highlight-here");
}
}
/***********************************************************************/
/* Replace an include-link with the given content (a DocumentFragment).
*/
// Called by: Transclude.transclude
function includeContent(includeLink, content) {
GWLog("includeContent", "transclude.js", 2);
/* We skip include-links for which a transclude operation is already in
progress or has completed (which might happen if we’re given an
include-link to process, but that link has already been replaced by its
transcluded content and has been removed from the document).
*/
if (includeLink.classList.containsAnyOf([
"include-in-progress",
"include-complete"
])) return;
// Where to inject?
let insertWhere = includeLink;
if (includeLink.classList.contains("include-replace-container")) {
/* This code block implements the `include-replace-container` class
and the `data-replace-container-selector` and
`data-replace-container-options` attributes.
(See documentation, at the start of this file, for details.)
*/
// Parse `data-replace-container-options` attribute.
let replaceContainerOptions = parsePipedOptions(includeLink.dataset.replaceContainerOptions);
// Find the container to replace.
let replacedContainer = null;
let replaceContainerSelectors = parsePipedOptions(includeLink.dataset.replaceContainerSelector);
if (replaceContainerSelectors) {
/* If `data-replace-container-selector` is specified, we try each
specified selector in turn...
*/
while ( replacedContainer == null
&& replaceContainerSelectors.length > 0)
replacedContainer = includeLink.closest(replaceContainerSelectors.shift());
/* ... and if they all fail, we default to the immediate parent,
or else no replacement at all (the `strict` option).
*/
if ( replacedContainer == null
&& replaceContainerOptions?.includes("strict") != true)
replacedContainer = includeLink.parentElement;
} else {
/* If `data-replace-container-selector` is not specified, we simply
replace the immediate parent.
*/
replacedContainer = includeLink.parentElement;
}
if (replacedContainer)
insertWhere = replacedContainer;
}
/* Just in case, do nothing if the element-to-be-replaced (either the
include-link itself, or its container, as appropriate) isn’t attached
to anything.
*/
if (insertWhere.parentNode == null)
return;
// Prevent race condition, part I.
includeLink.classList.add("include-in-progress");
// Document into which the transclusion is being done.
let containingDocument = includeLink.eventInfo.document;
let transcludingIntoFullPage = (containingDocument.querySelector("#page-metadata") != null);
// WITHIN-WRAPPER MODIFICATIONS BEGIN
// Wrap (unwrapping first, if need be).
let wrapper = newElement("SPAN", { "class": "include-wrapper" });
if ( includeLink.classList.contains("include-unwrap")
&& isAnchorLink(includeLink)
&& content.childElementCount == 1) {
wrapper.id = content.firstElementChild.id;
wrapper.append(...content.firstElementChild.childNodes);
} else {
wrapper.append(content);
}
// Inject wrapper.
insertWhere.parentNode.insertBefore(wrapper, insertWhere);
// Determine whether to “localize” content.
let shouldLocalize = shouldLocalizeContentFromLink(includeLink);
/* When transcluding into a full page, delete various “metadata” sections
such as page-metadata, footnotes, etc. (Save references to some.)
*/
let shouldMergeFootnotes = false;
let newContentFootnotesSection = wrapper.querySelector("#footnotes");
if (transcludingIntoFullPage) {
let metadataSectionsSelector = [
"#page-metadata",
"#footnotes",
"#further-reading",
"#similars-section",
"#link-bibliography-section"
].join(", ");
wrapper.querySelectorAll(metadataSectionsSelector).forEach(section => {
section.remove();
});
shouldMergeFootnotes = ( shouldLocalize
&& newContentFootnotesSection != null);
}
// ID transplantation.
if ( includeLink.id > ""
&& includeLink.classList.contains("include-identify-not") == false
&& wrapper.querySelector(`#${(CSS.escape(includeLink.id))}`) == null) {
let idBearerBlock = newElement("DIV", { "id": includeLink.id, "class": "include-wrapper-block" });
idBearerBlock.append(...wrapper.childNodes);
wrapper.append(idBearerBlock);
}
// Clear loading state of all include-links.
Transclude.allIncludeLinksInContainer(wrapper).forEach(Transclude.clearLinkState);
// Fire GW.contentDidInject event.
let flags = GW.contentDidInjectEventFlags.clickable;
if (containingDocument == document)
flags |= GW.contentDidInjectEventFlags.fullWidthPossible;
if (shouldLocalize)
flags |= GW.contentDidInjectEventFlags.localize;
if (shouldMergeFootnotes)
flags |= GW.contentDidInjectEventFlags.mergeFootnotes;
GW.notificationCenter.fireEvent("GW.contentDidInject", {
source: "transclude",
contentType: contentTypeIdentifierForIncludeLink(includeLink),
context: includeLink.eventInfo.context,
container: wrapper,
document: containingDocument,
loadLocation: loadLocationForIncludeLink(includeLink),
flags: flags,
includeLink: includeLink
});
// WITHIN-WRAPPER MODIFICATIONS END; OTHER MODIFICATIONS BEGIN
// Remove extraneous text node after link, if any.
if ( includeLink.classList.contains("include-replace-container") == false
&& includeLink.nextSibling
&& includeLink.nextSibling.nodeType == Node.TEXT_NODE) {
let cleanedNodeContents = Typography.processString(includeLink.nextSibling.textContent, Typography.replacementTypes.CLEAN);
if ( cleanedNodeContents.match(/\S/) == null
|| cleanedNodeContents == ".")
includeLink.parentNode.removeChild(includeLink.nextSibling);
}
// Remove include-link (along with container, if specified).
insertWhere.remove();
// Intelligent rectification of surrounding HTML structure.
if ( includeLink.classList.contains("include-rectify-not") == false
&& firstBlockOf(wrapper) != null) {
let allowedParentTags = [ "SECTION", "DIV", "LI", "BLOCKQUOTE", "FIGCAPTION" ];
while ( wrapper.parentElement != null
&& allowedParentTags.includes(wrapper.parentElement.tagName) == false
&& wrapper.parentElement.parentElement != null) {
let nextNode = wrapper.nextSibling;