Index: crystalize.rkt ================================================================== --- crystalize.rkt +++ crystalize.rkt @@ -51,10 +51,11 @@ list/articles+notes listing<>-short/articles listing<>-full/articles listing<>-full/articles+notes unfence + sqltools:dbc preheat-series!) ;; ~~~ Private use ~~~ (define DBFILE (build-path (current-project-root) "vitreous.sqlite")) @@ -147,11 +148,11 @@ [header (html$-article-open pagenode title-specified? title-tx pubdate)] [footertext (make-article-footertext pagenode series-node disposition disp-note-id (length note-txprs))] [footer (html$-article-close footertext)] [notes-section-html (crystalize-notes! pagenode title-plain note-txprs)]) - (crystalize-index-entries! pagenode body-txpr) + (crystalize-index-entries! pagenode doc) ; Note the original doc is used here ;; Values must come in the order defined in table_article_fields (define article-record (list (symbol->string pagenode) title-plain ADDED keyword-index.rkt Index: keyword-index.rkt ================================================================== --- keyword-index.rkt +++ keyword-index.rkt @@ -0,0 +1,117 @@ +#lang pollen/mode racket/base + +;; Copyright (c) 2019 Joel Dueck. +;; +;; Licensed under the Apache License, Version 2.0 (the "License"); +;; you may not use this file except in compliance with the License. +;; A copy of the License is included with this source code, in the +;; file "LICENSE.txt". +;; You may also obtain a copy of the License at +;; +;; http://www.apache.org/licenses/LICENSE-2.0 +;; +;; Unless required by applicable law or agreed to in writing, software +;; distributed under the License is distributed on an "AS IS" BASIS, +;; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +;; See the License for the specific language governing permissions and +;; limitations under the License. +;; +;; Author contact information: +;; joel@jdueck.net +;; https://joeldueck.com +;; ------------------------------------------------------------------------- + +;; 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{ + + + ◊html$-page-head{The Local Yarn: Keyword Index} + ◊html$-page-body-open[] + +
+ ◊the-index +
+ ◊html$-page-body-close[] + }) + +(define (main) + (spell-of-summoning!) ; Turn on DB + (displayln "Writing keyword-index.html…") + (display-to-file (html$-keywordindex-page (html$-index (group-entries (fetch-entries)))) + "keyword-index.html" + #:mode 'text + #:exists 'replace)) Index: makefile ================================================================== --- makefile +++ makefile @@ -23,11 +23,11 @@ # ~~~ Rules ~~ # # The order of these dependencies is important. They will be processed left to right. -web: _article_htmls.mark $(articles-html) $(series-html) blog-pg1.html +web: _article_htmls.mark $(articles-html) $(series-html) blog-pg1.html keyword-index.html web: ## Rebuild all web content (not PDFs) # The file article_htmls.mark is a zero-byte file that serves only as a marker. If it is older than # any of its dependencies (or missing) all of the articles will be rebuilt. Its dependencies are # also on the Pollen cache watchlist (see pollen.rkt) @@ -47,14 +47,17 @@ # Note that if any article is part of a series, it will touch its series .poly.pm file during its # render, triggering this rule for that series. $(series-html): %.html: %.poly.pm raco pollen render $@ -# This target will also rebuild pg2, pg3, etc. +# This target will also rebuild pg2, pg3, etc. as needed blog-pg1.html: $(core-files) $(html-deps) $(articles-html) blog.rkt rm -f blog*.html racket -tm blog.rkt + +keyword-index.html: $(core-files) $(html-deps) $(articles-html) keyword-index.rkt + racket -tm keyword-index.rkt spritz: ## Clear Pollen and Scribble cache rm -rf compiled code-docs/compiled articles/compiled series/compiled fossil clean code-docs/ Index: tags-html.rkt ================================================================== --- tags-html.rkt +++ tags-html.rkt @@ -118,10 +118,11 @@ (cond [(string>? file "") `(@ (div [[class "listing-filename"]] 128196 " " ,file) ,codeblock)] [else codeblock])) (define (html-index . elems) `(a [[id ,(here-id (list "_idx-" (uri-encode (car elems))))] + [href ,(string-append "/keyword-index.html#" (uri-encode (string-downcase (car elems))))] [data-index-entry ,(car elems)] [class "index-link"]] ,@(cdr elems))) (define (html-say . elems) Index: web-extra/martin.css.pp ================================================================== --- web-extra/martin.css.pp +++ web-extra/martin.css.pp @@ -187,11 +187,11 @@ span.links-footnote { display: inline-block; /* allows keyframe animation to work */ } - li:target, a:target, span.links-footnote:target { + :target { animation: hilite 2.5s; } @keyframes hilite { 0% {background: transparent;} 10% {background: #feffc1;} @@ -640,10 +640,34 @@ } div.article-list-title { font-size: 1.2rem; } + + /* ******* (Mobile first) Keyword Index styling ******* + */ + + #keywordindex { + column-width: 7rem; + margin-top: ◊x-lineheight[2]; + } + + #keywordindex section { + -webkit-column-break-inside: avoid; /* Chrome, Safari, Opera */ + page-break-inside: avoid; /* Firefox */ + break-inside: avoid; /* IE 10+ */ + } + + #keywordindex h2 { + margin: 0; + } + + #keywordindex ul { + margin-top: 0; + list-style-type: none; + padding: 0; + } /* End of mobile-first typography and layout */ }