-
Notifications
You must be signed in to change notification settings - Fork 32
/
pages.tex
832 lines (716 loc) · 28.5 KB
/
pages.tex
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
\chapter{Manipulating Pages}\label{s:pages}
We have presented a lot of tools, but as yet no applications.
As a reward for your patience,
we will therefore work through several examples that show
how to do useful things to web pages.
These examples introduce some new concepts,
the most important of which is the way in which HTML pages are represented in,
and manipulated by,
JavaScript.
One thing these examples \emph{don't} show is how to build interactive web pages.
JavaScript was invented primarily to support buttons, forms, and the like,
but we will need a bit more background before exploring them.
Still,
we can do a surprising number of useful things
simply by playing with the content of pages.
\section{Counting Paragraphs}\label{s:pages-counting}
Let's begin by counting the number of paragraphs in a page:
\begin{minted}{html}
<html>
<head>
<meta charset="utf-8"/>
</head>
<body>
<h1>Title</h1>
<div id="fill"></div>
<h2 id="one">First <em>emphasized</em></h2>
<p>stuff</p>
<h2 id="two">Second <code>with code</code></h2>
<h3>stuff</h3>
<h2 id="three">Third</h2>
<p>stuff</p>
<script>
const counter = () => {
const paragraphs = document.querySelectorAll('p')
return paragraphs.length
}
console.log(`number of paragraphs: ${counter()}`)
</script>
</body>
</html>
\end{minted}
This page has three main parts:
\begin{enumerate}
\item
The \texttt{head} contains a \texttt{meta} tag that specifies the page's
\gref{g:character-encoding}{character encoding},
i.e.,
the scheme used to represent characters
not found on a standard American keyboard in the 1970s.
Character sets and character encodings are out of scope for this lesson;
see \hreffoot{https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/}{this essay}
for an unfortunately timeless discussion.
\item
The top half of the \texttt{body} has some headings and paragraphs
for the JavaScript to play with.
It also contains a \texttt{div} marked with \texttt{class="fill"}
that our script will eventually fill in with a count.
\item
The script itself is contained in a \texttt{script} tag at the bottom of the page;
we will explore it in depth below.
\end{enumerate}
\begin{aside}{When Scripts Run}
We have put the script at the bottom of the page
because we want to be sure that the page's contents actually exist in memory
before trying to process them.\index{running JavaScript!in the browser}
If we put the \texttt{script} tag and its contents at the top of the page,
the browser might run our JavaScript \emph{after} the page has been read
but \emph{before} its elements and text have been parsed and stored in memory.
\grefdex{g:race-condition}{Race conditions}{race condition} like this bedevil web programming;
we will see more robust ways to deal with them later.
\end{aside}
Inside the \texttt{script} tag,
we define a function called \texttt{counter} that takes no arguments,
then use \texttt{console.log} to display the result of calling it.
The only thing inside the function that we haven't encountered before
is the call \texttt{document.querySelectorAll('p')}.\index{query selector}
As you might guess from its name,
\texttt{document} is an object that gives us a handle on the page the script is in;
it is created and provided automatically by the browser.
Its \texttt{querySelectorAll} method finds all elements in the page
that match the selector we provide.
In this case,
we're looking for all paragraphs,
so we simply search for \texttt{'p'}.
To see the JavaScript in action,
run a browser,
open its developer tools so that you can see the JavaScript console,
and then load the page.
The page's elements will be displayed as usual,
and the console will show:
\begin{minted}{text}
number of paragraphs: 2
\end{minted}
\begin{aside}{Developer Tools}
To open developer tools in Firefox,\index{developer tools}
go to the main menu and select \texttt{Tools {\textgreater} Web\ Developer {\textgreater} Toggle\ Tools}.
A tabbed display will open in the bottom of your page;
choose \texttt{Console} to view the output of your JavaScript,
or to write a little bit to run immediately.
You can open a similar set of tools from \texttt{View {\textgreater} Developer {\textgreater} JavaScript\ Console} in Chrome,
or by using \texttt{Ctrl+Shift+I} on Windows for Firefox, Chrome, or Microsoft Edge.
\end{aside}
Showing results in the console is good enough for development work,
but we would like to see the result in the page itself.
To do this,
we can replace the call to \texttt{console.log} with the two lines shown below:
\begin{minted}{js}
const counter = () => {
const paragraphs = document.querySelectorAll('p')
return paragraphs.length
}
const fill = document.getElementById('fill')
fill.innerHTML = `number of paragraphs: ${counter()}`
\end{minted}
Where \texttt{document.querySelectorAll} returns all nodes that match a selector,
\texttt{document.getElementById} returns a single element that has the specified ID
(which is set inside the element's opening tag with \texttt{id="some\_name"}).
The variable \texttt{fill} is therefore assigned a reference to our \texttt{div}.
We can then change the text inside that element by assigning to
its \texttt{innerHTML} property.\index{innerHTML property@\texttt{innerHTML} property}
When we do this,
JavaScript parses the string we provided as if it were HTML
and creates whatever nodes it needs to represent the result.
In this case,
the content is just text,
so JavaScript will create a single text node,
store \texttt{"number\ of\ paragraphs:\ 2"} as its content,
and add it to the in-memory structure that represents the page.
{\ldots}at which point some magic happens behind the scenes.
The browser stores the elements and text of the current page in a data structure called
the Document Object Model,
or more commonly the \gref{g:dom}{DOM}.\index{Document Object Model}
As shown in \figref{f:htmlcss-tree},
the DOM is organized as a tree:
each element or piece of text is a \grefdex{g:node}{node}{node (of tree)}\index{tree!node} in the tree,
and a node's children are the elements contained within it.
Any time the browser detects a change to the DOM,
it automatically refreshes just as much of its display as it needs to.
We can insert or remove text,
change elements' styles,
or copy in entire sub-pages:
each time,
the browser will do only the work required to reflect that change
as quickly as possible.
\section{Creating a Table of Contents}\label{s:pages-toc}
Reporting the number of paragraphs is a good way to see how JavaScript works in the browser,
but isn't particularly useful
(although counting the number of words is---we will tackle that in the exercises).
Something we're more likely to put in a real page is a table of contents,
which takes only a little more code than what we've already seen:
\begin{minted}{js}
(() => {
const container = document.getElementById('fill')
const headings = Array.from(document.querySelectorAll('h2'))
const items = headings
.map((h) => `<li><a href="#${h.id}">${h.innerHTML}</a></li>`)
.join('')
container.innerHTML = '<ul>' + items + '</ul>'
})()
\end{minted}
Let's start with the first and last lines,
since they demonstrate a commonly-used idiom.
We've seen how to define a function and then call it:
\begin{minted}{js}
const f = (param) => {
// body
}
f()
\end{minted}
If we're only going to call the function once,
we might as well define it and call it immediately without giving it a name:
\begin{minted}{js}
(param) => {
// body
}(actual_value)
\end{minted}
\noindent
However,
this doesn't reliably work as written;
in order to make JavaScript happy,
we have to put parentheses around the function definition
so that it's clear exactly what's being called:
\begin{minted}{js}
((param) => {
// body
})(actual_value)
\end{minted}
\texttt{()} before the fat arrow means ``this function doesn't take any parameters''.
The second \texttt{()} after the closing curly brace means ``call the function''.
If the function doesn't take any arguments,
this becomes:
\begin{minted}{js}
(() => {
// body
})()
\end{minted}
\noindent
which is a lot of parentheses in a row,
but that's what people write.
Let's come back to the body of the function:
\begin{minted}{js}
const container = document.getElementById('fill')
const headings = Array.from(document.querySelectorAll('h2'))
const items = headings
.map((h) => `<li><a href="#${h.id}">${h.innerHTML}</a></li>`)
.join('')
container.innerHTML = '<ul>' + items + '</ul>'
\end{minted}
\noindent
As before,
the first line gets the \texttt{div} we're going to fill in.
The second line grabs all the \texttt{h2} headings,
which we have arbitrarily decided are the only things worthy of inclusion
in the table of contents.
We run the result of \texttt{document.querySelectorAll}
through the function \texttt{Array.from}
because the former's result isn't a JavaScript array:
for reasons that probably made sense to someone, somewhere,
it's a thing called a \texttt{NodeList}
that lacks most of \texttt{Array}'s useful methods.
\begin{aside}{Static Methods}
\texttt{Array.from} is a \gref{g:static-method}{static method}:
it belongs to the class as a whole,
not to objects of that class.
Static methods are primarily used to associate utility functions with classes,
like \texttt{dist} in the example below.
Unlike calls to \grefdex{g:instance-method}{instance methods}{instance method} like \texttt{magnitude},
static methods are called using \texttt{class.method()}
rather than \texttt{some\_object.method()}.
\end{aside}
We then have three lines that do most of the function's work.
The first tells us that \texttt{items} is going to be assigned
something derived from \texttt{headings};
the second transforms the array of headings into an array of strings,
and the third joins those strings to create a single string.
Looking at the \texttt{map} call,
each heading becomes a list item (\texttt{li})
containing a link (\texttt{a})
whose \texttt{href} attribute is the ID of the heading
and whose displayed content (the text between \texttt{\htmltag{a...}} and \texttt{\htmltag{/a}})
is the text of the heading.
The \texttt{href} attribute's value starts with \texttt{\#},
which makes this a local link
(i.e., it links to something inside the same page).
If one of our \texttt{h2} headings doesn't have an \texttt{id} set,
this \texttt{map} will fail;
we'll explore ways to handle this in the exercises.
Finally,
the last line of the code shown above
fills in the content of the container (i.e., the \texttt{div})
with an unordered list (\texttt{ul})
that contains all of the items we just constructed.
Again,
when we assign to an element's \texttt{innerHTML} property,
JavaScript parses the string we give it
and constructs the HTML nodes we need.
It would be marginally faster to build these nodes ourselves
(which we will do in the exercises),
but building and parsing strings is usually easier to read,
and the performance differences are small enough in modern browsers
that we should only worry about them if they actually prove themselves a problem.
\section{Sortable Lists}\label{s:pages-sort-list}
Creating nodes allows us to add content to a page,
but we can also rearrange the nodes that are there (\figref{f:pages-sortlist}).
Our next exercise is to sort the elements of a list,
so that if the author writes:
\begin{minted}{html}
<ul>
<li>pee (P)</li>
<li>cue (Q)</li>
<li>are (R)</li>
</ul>
\end{minted}
\noindent
we will automatically rearrange the items to be:
\begin{minted}{html}
<ul>
<li>are (R)</li>
<li>cue (Q)</li>
<li>pee (P)</li>
</ul>
\end{minted}
Our first attempt uses this as the HTML page:
\begin{minted}{html}
<html>
<head>
<meta charset="utf-8">
<script src="sort-lists.js"></script>
</head>
<body onload="sortLists()">
<ul class="sorted">
<li>first</li>
<li>second</li>
<li>third</li>
<li>fourth</li>
<li>fifth</li>
</ul>
<ol class="sorted">
<li>one</li>
<li>two</li>
<li>three</li>
<li>four</li>
<li>five</li>
</ol>
</body>
</html>
\end{minted}
\noindent
and this as our initial script:
\begin{minted}{js}
const sortLists = () => {
const lists = Array.from(document.querySelectorAll('#sorted'))
lists.forEach((list) => {
const children = Array.from(list.childNodes)
.filter(c => c.nodeName !== '#text')
children.sort((left, right) =>
left.textContent.localeCompare(right.textContent))
while (list.firstChild) {
list.removeChild(list.firstChild)
}
children.forEach(c => list.appendChild(c))
})
}
\end{minted}
When we load the page,
though,
the items aren't sorted.
A bit of trial and error reveals that we have tripped over the race condition alluded to earlier:
if we call our function in the \texttt{onload} attribute\index{onload attribute (of page)@\texttt{onload} attribute (of page)}
of the \texttt{body} tag,
it is run when the page is loaded into memory
but \emph{before} the page's content has been parsed and turned into a DOM tree.
After searching online for ``run JavaScript when page loaded'',
we go back to this:
\begin{minted}{html}
<html>
<head>
<meta charset="utf-8">
<script src="sort-lists-event.js"></script>
</head>
<body>
...lists as before...
</body>
</html>
\end{minted}
\noindent
and write our JavaScript like this:
\begin{minted}{js}
const sortLists = () => {
// ...function to sort lists...
}
document.addEventListener("DOMContentLoaded", (event) => {
sortLists()
})
\end{minted}
An \gref{g:event-listener}{event listener} is a function that the browser calls
when some kind of event occurs.
In our example,
the event we care about is ``DOM content has been loaded''.
When that occurs,
the browser will call \texttt{sortLists()}.
(The \texttt{event} parameter to our function will be given an object
that stores details about what precisely happened.
We don't need that information now,
but will use it later when we start handling button clicks and the like.)
Let's return to the function:
\begin{minted}{js}
const sortLists = () => {
const lists = Array.from(document.querySelectorAll('.sorted'))
lists.forEach((list) => {
const children = Array.from(list.childNodes)
.filter(c => c.nodeName !== '#text')
children.sort((left, right) =>
left.textContent.localeCompare(right.textContent))
while (list.firstChild) {
list.removeChild(list.firstChild)
}
children.forEach(c => list.appendChild(c))
})
}
\end{minted}
\figpdf{figures/pages-sortlist.pdf}{Sorting and Replacing}{f:pages-sortlist}
As before,
it starts by creating an array containing the nodes we want to operate on.
(We use the selector \texttt{.sorted} (with a leading dot) to select everything with the class \texttt{sorted},
rather than \texttt{\#sorted},
which would find nodes with the ID \texttt{sorted}.)
This array will then have all the \texttt{ul} or \texttt{ol} lists that the function is to sort.
We process each list separately with \texttt{lists.forEach}.
The callback function inside \texttt{forEach}
uses \texttt{Array.from} to create an array containing the child nodes of the main list element,
then filters that list to remove any that are text nodes.
We need the \texttt{Array.from} call because (once again) the DOM doesn't use a JavaScript array to store children,
but a structure of its own devising that lacks the methods we want to call.
We remove the text nodes because they represent the \gref{g:whitespace}{whitespace} between list elements,
such as newline characters after a closing \texttt{{\textless}/li{\textgreater}}
and before the next opening \texttt{{\textless}li{\textgreater}}.
\begin{aside}{Identifying Text Nodes}
We could check \texttt{c.nodeType} instead of \texttt{c.nodeName} to spot text nodes,
but we felt that \texttt{nodeName} made the code easier to understand.
As always,
we use \texttt{!==} for the comparison
in order to prevent unpleasant surprises (\secref{s:legacy-equality}).
\end{aside}
Now that we have an array of the \texttt{li} elements to be sorted,
we can use \texttt{Array.sort} to order them.
Since we want to sort them by the text they contain,
we have to provide our own sorting function
that returns a negative number, 0, or a positive number to show whether its \texttt{left} argument is less than,
equal to,
or greater than its \texttt{right} argument.
We use the \texttt{textContent} member of the node to get the text it contains,
and the string object's \texttt{localeCompare} to get a -1/0/1 result.
All of this was discovered by searching online,
primarily on the \hreffoot{https://www.w3schools.com/}{W3Schools} site.
Unfortunately,
searching for ``remove all children from node'' tells us that we have to do it ourselves,
so we use a \texttt{while} loop to remove all the children\index{while loop@\texttt{while} loop}\index{loop!\texttt{while}}
(including the unwanted top-level text elements)
from the \texttt{ul} or \texttt{ol} list,
then add all of the children back in sorted order.
Sure enough,
the page now displays the nodes in the right order.
\section{Bibliographic Citations}\label{s:pages-citations}
And so we come to the largest example in this lesson.
HTML has a \texttt{cite} tag for formatting citations,
but it doesn't allow us to link directly to a bibliography entry.
In order to minimize typing in scholarly papers,
we'd like to find links like this:
\begin{minted}{html}
<a href="#b">key1, key2</a>
\end{minted}
\noindent
and turn them into this:
\begin{minted}{html}
[<a href="../bib/#key1">key1</a>, <a href="../bib/#key2">key2</a>]
\end{minted}
The typed-in form is about as little typing as we can get away with;
the displayed form then wraps the citations in \texttt{[...]}
and turns each individual citation into a link to our bibliography.
For now,
we'll assume that the bibliography can be found at \texttt{../bib/},
i.e.,
in a file called \texttt{index.html} that's in a directory called \texttt{bib}
that's a sibling of the directory containing whatever page the citation is in.
This is very fragile,
and we should be ashamed of ourselves,
but we can tell ourselves that we're going to fix it later
and get on with learning JavaScript for now.
Our test page contains two bibliographic links
and one non-bibliographic link:
\begin{minted}{html}
<html>
<head>
<meta charset="utf-8">
<script src="citations.js"></script>
</head>
<body>
<p>As <a href="#b">Moreau1896</a> shows...</p>
<p>
We can look to <a href="#b">Brundle1982, Brundle1984</a>
for answers.
</p>
<hr/>
<p><em>Visit <a href="http://somewhere.org">the author's site</a>.</em></p>
</body>
</html>
\end{minted}
\noindent
Here's our function (\figref{f:pages-pipeline}),
which we'll call from an event listener as before:
\begin{minted}{js}
const citations = () => {
Array.from(document.querySelectorAll('a'))
.filter(link => link.getAttribute('href') === '#b')
.map(link => (
{node: link,
text: link.textContent.split(',').map(s => s.trim())}
))
.map(({node, text}) => (
{node,
text: text.map(cite => `<a href="../bib/#${cite}">${cite}</a>`)}
))
.map(({node, text}) => (
{node,
text: `[${text.join(', ')}]`}
))
.forEach(({node, text}) => {
const span = document.createElement('span')
span.innerHTML = text
node.parentNode.replaceChild(span, node)
})
}
\end{minted}
\figpdf{figures/pages-pipeline.pdf}{A Processing Pipeline}{f:pages-pipeline}
There is a lot going on here,
but it all uses patterns we have seen before.
It starts by building an array of all the links in the document
i.e., every \texttt{a} element:
\begin{minted}{js}
Array.from(document.querySelectorAll('a'))
// output:
// - <a href="#b">Moreau1896</a>
// - <a href="#b">Brundle1982, Brundle1984</a>
// - <a href="http://somewhere.org">the author's site</a>
\end{minted}
\noindent
(We show the nodes in comments to visualize what each step of the pipeline does.)
We then filter this array to find the links pointing at \texttt{\#b},
which is what we're using to signal citations:
\begin{minted}{js}
.filter(link => link.getAttribute('href') === '#b')
// output:
// - <a href="#b">Moreau1896</a>
// - <a href="#b">Brundle1982, Brundle1984</a>
\end{minted}
We now have a problem.
We could use a \texttt{map} call to get the text out of each link and process it,
but then all we'd have is an array of strings.
We're going to want the nodes those strings came out of later on as well
so that we can modify their \texttt{href} attributes,
so somehow we have to pass the nodes and strings together through our pipeline.
One option would be to create a two-element array for each:
\begin{minted}{js}
.map(link => [link, link.textContent.whatever])
\end{minted}
\noindent
but it's more readable to create an object so that each component has a name:
\begin{minted}{js}
.map(link => (
{node: link,
text: link.textContent.split(',').map(s => s.trim())}
))
// output:
// - {node: <a href="#b">Moreau1896</a>,
// text: ['Moreau1896']}
// - {node: <a href="#b">Brundle1982, Brundle1984</a>,
// text: ['Brundle1982', 'Brundle1984']}
\end{minted}
Here,
we are turning each link into an object whose \texttt{"node"} key has the link's DOM node as its value,
and whose \texttt{"text"} key has the node's text,
which we split on commas and trim to remove leading and trailing whitespace.
But we're not done looking at this stage of our pipeline:
\begin{enumerate}
\item
We don't need to quote the names \texttt{"node"} and \texttt{"text"}, though we could.
\item
JavaScript's \texttt{String.split} returns an array,
so the value associated with \texttt{"text"} is an array.
We then \texttt{map} over its elements to trim leading and trailing space from each.
\item
If we wrote \texttt{link\ ={\textgreater}\ \{node:\ link,\ text:\ whatever\}},
JavaScript would interpret the curly braces \texttt{\{...\}} as meaning,
``Here is the body of a function,''
and then complain because what's in those curly braces clearly \emph{isn't} a function body.
Putting parentheses around the curly braces,
i.e., writing \texttt{(\{...\})},
tells JavaScript that the function is returning an object.
\end{enumerate}
After all of this,
the next stage of the pipeline is almost a relief:
\begin{minted}{js}
.map(({node, text}) => (
{node,
text: text.map(cite => `<a href="../bib/#${cite}">${cite}</a>`)}
))
// output:
// - {node: <a href="#b">Moreau1896</a>,
// text: [<a href="../bib/#Moreau1896"Moreau1896</a>]}
// - {node: <a href="#b">Brundle1982, Brundle1984</a>,
// text: [<a href="../bib/#Brundle1982">Brundle1982</a>,
// <a href="../bib/#Brundle1984">Brundle1984</a>]}
\end{minted}
All right,
that's not actually much of a relief,
but it does make a strange kind of sense.
First,
if we have an object whose keys are called \texttt{a} and \texttt{b},
then the call \texttt{f(\{a,\ b\})} means,
``Match the value of key \texttt{a} to a parameter called \texttt{a}
and the value of key \texttt{b} to a parameter called \texttt{b}.''
This is called \gref{g:destructuring}{destructuring},
and can save a lot of wear and tear on our keyboard and eyes.
Second,
if we have a variable called \texttt{name},
then define an object with \texttt{\{name\}},
JavaScript helpfully assumes that what we mean is \texttt{\{"name":\ name\}},
i.e.,
that we want a key called \texttt{"name"}
with whatever value \texttt{name} currently has.
This allows us to pass the value of \texttt{node} from call to call in our pipeline
without typing anything more than its name.
And after all of \emph{this},
the \texttt{text.map} call actually \emph{is} a relief.
The value associated with the key \texttt{text} is an array of strings,
each of which is a bibliography key.
All the \texttt{map} does is convert each to the text we want:
a link that refers to \texttt{../bib/\#citation\_key} and whose displayed text is also the citation key.
On to the next stage,
which joins the string in \texttt{text} together to create a single string
with commas between the entries
and square brackets around the whole thing:
\begin{minted}{js}
.map(({node, text}) => (
{node,
text: `[${text.join(', ')}]`}
))
\end{minted}
\noindent
(We haven't shown the output in comments because typesetting it would overflow the page
and because our pseudo-HTML notation gets really confusing
once we're showing strings containing HTML that contains strings.)
The last stage in our pipeline uses \texttt{forEach} instead of \texttt{map}
because we want to do something for each element of the array,
but don't need a value returned
(because what we're doing has the side effect of modifying the document):
\begin{minted}{js}
.forEach(({node, text}) => {
const span = document.createElement('span')
span.innerHTML = text
node.parentNode.replaceChild(span, node)
})
\end{minted}
This is the point at which carrying the node itself forward through the pipeline pays off.
We create a \texttt{span} element,
fill it in by assigning to its \texttt{innerHTML} property,
and then replace the original link node with the node we have just created.
If we now add an event listener after our function to call it when the page is loaded,
we see our citations formatted as we desired.
\section{A Real-time Clock}\label{s:pages-clock}
We will wrap up this lesson with an example that is short,
but hints at the possibilities to come:
\begin{minted}{js}
<html>
<head>
<script>
const startTime = () => {
const today = new Date()
const fields = [today.getHours(),
today.getMinutes(),
today.getSeconds()]
const current = fields
.map(t => `${t}`.padStart(2, '0'))
.join(':')
document.getElementById('current').innerHTML = current
setTimeout(startTime, 1000)
}
document.addEventListener("DOMContentLoaded", (event) => {
startTime()
})
</script>
</head>
<body>
<p id="current"></p>
</body>
</html>
\end{minted}
Defining a function: check.
Calling that function when the DOM is ready: check.
What about inside the function?
It's pretty easy to guess that \texttt{Date()} creates an object that holds a date,
and from the fact that we're assigning that object to a variable called \texttt{today},
you might even guess that if we don't specify which date we want,
we get today's values.
We then pull the hours, minutes, and seconds out of the date and put them in an array
so that we can turn each value into a two-character string,
padded with a leading zero if necessary,
and then join those strings to create a time like \texttt{17:48:02}
to stuff into the element whose ID is \texttt{current}.
But what does \texttt{setTimeout} do?\index{setTimeout|texttt}
It tells the browser to run a function after some number of milliseconds have passed.
In this case,
we're running the same function \texttt{startTime} a second from now.
That call will change the displayed time,
then set up another callback to run a second later,
and so on forever.
When we load the page,
we see the current time updating itself second by second
to remind us just how quickly life is passing by.
\section{Exercises}\label{s:pages-exercises}
\exercise{What Encoding Is This?}
\begin{enumerate}
\item
Write a function that looks up the character encoding of the page the script is in
and prints it to the console.
\item
Extend the function to look up all the \texttt{meta} tags in the current page
and print their names and values.
\end{enumerate}
\exercise{Word Count}
\begin{enumerate}
\item
Write a function called \texttt{countWords} that finds all the text nodes in a page,
splits them on whitespace,
and returns the total number of words in the page.
\item
Write a second function called \texttt{showWords} that uses the first to find the number of words,
then displays that number in a paragraph whose ID is \texttt{wordcount}.
\end{enumerate}
\exercise{A More Robust Table of Contents}
\begin{enumerate}
\item
What does the table of contents example generate if an \texttt{h2} \emph{doesn't} have an \texttt{id} attribute?
\item
Modify the example so that it only includes \texttt{h2} elements that have an \texttt{id} attribute
in the table of contents.
\end{enumerate}
\exercise{Explicitly Creating Nodes}
Find documentation online for the two functions
\texttt{document.createElement} and \texttt{document.createTextNode},
then rewrite the table of contents example to use these methods
(and any others like them that you need)
instead of assigning to a node's \texttt{innerHTML} property.
\section*{Key Points}
\input{keypoints/pages}