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
|
#lang pollen/mode racket/base
; SPDX-License-Identifier: BlueOak-1.0.0
; This file is licensed under the Blue Oak Model License 1.0.0.
;; Builds an HTML page containing the keyword index for all ◊index entries in
;; the articles, by pulling them out of the SQLite cache DB.
(require racket/match
racket/list
racket/file
db/base
net/uri-codec
pollen/template)
(require "crystalize.rkt"
"snippets-html.rkt")
(provide main)
;; Get the index entries from the SQLite cache, return them as a list of vectors
(define (fetch-entries)
(define q
◊string-append{
SELECT entry, subentry, a.rowid, "/" || k.pagenode || "#" || anchor AS href, title_plain
FROM keywordindex k INNER JOIN articles a
ON a.pagenode = k.pagenode
ORDER BY entry COLLATE NOCASE ASC;})
(query-rows (sqltools:dbc) q))
;; Convert a vector (row) into a txexpr representing a link to the original article
(define (make-link row)
`(a [[href ,(vector-ref row 3)]
[title ,(vector-ref row 4)]]
,(number->string (vector-ref row 2))))
;(require sugar/debug)
;; Convert a list of vectors from the cache DB into a list of the form:
;; (list (cons FIRST-LETTER
;; (list (cons KEYWORD
;; (list LINKS ...))
;; ...))
;; ...)
(define (group-entries data)
(define collated-list
(for/fold ([entry-table null]
#:result (reverse entry-table))
([row (in-list data)])
(define this-entry (vector-ref row 0))
(cond [(and (not (null? entry-table))
(string-ci=? (first (first entry-table)) this-entry))
(match-define (cons (list last-entry last-list) rest-entries) entry-table)
(cons `(,last-entry ,(append last-list (list (make-link row)))) rest-entries)]
[else
(cons `(,this-entry ,(list (make-link row)))
entry-table)])))
(define (first-letter entry)
(char-upcase (first (string->list (first entry)))))
(for/list ([letter-group (in-list (group-by first-letter collated-list))])
(list (first-letter (first letter-group)) letter-group)))
(define (entry+links->txexpr entry)
(match-define (list entry-word links) entry)
`(li [[id ,(uri-encode (string-downcase entry-word))]]
,entry-word nbsp
,@(add-between links ", ")))
;; Return the complete HTML for the keyword index. Each letter group begins with a heading for the
;; letter, followed by a definition list for its entries.
(define (html$-index entries)
(define groups
(for/list ([letter-group (in-list entries)])
(match-define (list letter-char entries) letter-group)
`(section (h2 ,(list->string (list letter-char)))
(ul ,@(map entry+links->txexpr entries)))))
(apply string-append (map ->html groups)))
(define (html$-keywordindex-page the-index)
◊string-append{
<!DOCTYPE html>
<html lang="en">
◊html$-page-head{The Local Yarn: Keyword Index}
◊html$-page-body-open[]
|
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
>
|
|
|
<
<
<
<
<
<
<
<
|
<
<
<
<
>
|
|
|
|
|
<
<
<
|
|
<
|
>
|
<
<
|
|
>
|
|
>
|
|
|
|
|
|
>
|
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
|
#lang pollen/mode racket/base
; SPDX-License-Identifier: BlueOak-1.0.0
; This file is licensed under the Blue Oak Model License 1.0.0.
;; Builds an HTML page containing the keyword index for all ◊index entries in
;; the articles, by pulling them out of the SQLite cache DB.
(require racket/match
racket/list
racket/file
racket/string
db/base
net/uri-codec
pollen/template)
(require "crystalize.rkt"
"dust.rkt"
"snippets-html.rkt")
(provide main)
;; Terminology (because these things get confusing fast)
;;
;; Index HEADING: A word or phrase that appears in the index
;; Index SUBHEADING: A Heading that appears under another Heading
;; Index LINK: The connection between a particular Article and a Heading.
;; In the Index, represented by a URL+fragment back into the Article.
;; In the Article, represented by a URL+fragment to the Heading in the Index.
;; Index ENTRY: A Heading, a unique ID, a list of Links, and a list of Subheading-entries
;; Index RECORD: A simple vector representing a Link the way it is fetched from the SQLite
;; cache: #("heading" "subheading" NUMBER "/absolute-path.html#id_fragment" "Article Title")
(struct entry (heading ; String
id ; String
links ; List
subentries) #:transparent) ; List of entry structs
;; Get the first letter of an entry’s heading
(define (first-letter e)
(char-upcase (first (string->list (entry-heading e)))))
;; Format the ID for back-linking to a particular entry on the keyword index page
(define (format-entry-id head subhead)
(define maybe-sub
(cond [(non-empty-string? subhead) (string-append "!" subhead)]
[else ""]))
(uri-encode (string-downcase (string-append head maybe-sub))))
;; Convert a Record into a txexpr representing a link to the original article
(define (make-link rec)
(match-define (vector _ _ num link-url article-title) rec)
`(a [[href ,link-url]
[title ,article-title]]
,(number->string num)))
;; See if a Record belongs under an entry's heading or under an entry's subheading.
;; For subheadings, only checks the first one! This is because we rely on the records
;; being pre-alphabetized as they are fetched from the SQLite cache, and being
;; processed in order.
(define (test-match e record)
(match-define (vector record-head record-subhead _ _ _) record)
(define heading-match?
(string-ci=? (entry-heading e) record-head))
(define subheading-match?
(and heading-match?
(not (null? (entry-subentries e)))
(non-empty-string? record-subhead)
(string-ci=? (entry-heading (first (entry-subentries e))) record-subhead)))
(cond [(and heading-match? subheading-match?) 'existing-subhead]
[(and heading-match?
(non-empty-string? record-subhead)) 'new-subhead]
[heading-match? 'heading]
[else 'not-a-match]))
;; Add a record’s link to the front of an entry’s list of links
(define (add-entry-link e rec)
(define existing-links (entry-links e))
(struct-copy entry e [links (cons (make-link rec) existing-links)]))
;; Add a record’s link to the first subheading given in an entry.
(define (add-to-first-subentry e record)
(match-define (entry _ _ _ (cons last-sub rest-subs)) e)
(define updated-sub (add-entry-link last-sub record))
(struct-copy entry e [subentries (cons updated-sub rest-subs)]))
;; Add a record's link to a new subheading in an entry
(define (add-new-subentry e record)
(match-define (vector head subhead _ _ _) record)
(define subhead-id (format-entry-id head subhead))
(define new-sub (entry subhead subhead-id (list (make-link record)) '()))
(struct-copy entry e [subentries (list new-sub)]))
;; Make a new entry from a record, placing its link in either the main entry
;; or in a new sub-entry.
(define (new-entry record)
(match-define (vector head subhead _ _ _) record)
(define e (entry head (format-entry-id head "") '() '()))
(cond [(non-empty-string? subhead) (add-new-subentry e record)]
[else (add-entry-link e record)]))
;; Get the index entries from the SQLite cache, return them as a list of vectors (Records!)
(define (fetch-entries)
(define q
◊string-append{
SELECT entry, subentry, a.rowid, "◊web-root" || k.pagenode || "#" || anchor AS href, title_plain
FROM keywordindex k INNER JOIN articles a
ON a.pagenode = k.pagenode
ORDER BY entry COLLATE NOCASE ASC, subentry COLLATE NOCASE ASC;})
(query-rows (sqltools:dbc) q))
;; Convert a list of vectors from the cache DB into a list of the form:
;; ((FIRST-LETTER (entries ...)) ...)
;; The method relies on the records being pre-sorted by the SQL query.
(define (group-entries records)
(define collated
(for/fold ([entries (list (new-entry (first records)))]
#:result (reverse entries))
([row (in-list (rest records))])
(match-define (cons last-entry rest-entries) entries)
(case (test-match last-entry row)
[(existing-subhead) (cons (add-to-first-subentry last-entry row) rest-entries)]
[(new-subhead) (cons (add-new-subentry last-entry row) rest-entries)]
[(heading) (cons (add-entry-link last-entry row) rest-entries)]
[else (cons (new-entry row) entries)])))
(for/list ([letter-group (in-list (group-by first-letter collated))])
(list (first-letter (first letter-group)) letter-group)))
;; Convert an entry into a list item, recursively adding subentries
(define (entry->txexpr e)
(match-define (entry head id links subentries) e)
(define sub-txs
(cond [(null? subentries) ""]
[else `(ul ,@(map entry->txexpr subentries))]))
`(li [[id ,id]] ,head nbsp ,@(add-between links ", ") ,sub-txs))
;; Return the complete HTML for the keyword index. Each letter group begins with a heading for the
;; letter, followed by a definition list for its entries.
(define (html$-index grouped-entries)
(define groups
(for/list ([letter-group (in-list grouped-entries)])
(match-define (list letter-char entries) letter-group)
`(section (h2 ,(list->string (list letter-char)))
(ul ,@(map entry->txexpr entries)))))
(apply string-append (map ->html groups)))
;; Build the complete HTML page
(define (html$-keywordindex-page the-index)
◊string-append{
<!DOCTYPE html>
<html lang="en">
◊html$-page-head{The Local Yarn: Keyword Index}
◊html$-page-body-open[]
|