Index: code-docs/dust.scrbl ================================================================== --- code-docs/dust.scrbl +++ code-docs/dust.scrbl @@ -56,10 +56,22 @@ @defproc[(here-output-path) path?] Returns the path to the current output file, relative to @racket[current-project-root]. If no metas are available, raises an error. This is like what you can get from the @tt{here} variable that Pollen provides, except it is available outside templates. + +@defproc[(here-id [suffix (or/c (listof string?) string? #f) #f]) string?] + +Returns the 8-character prefix of the SHA1 hash of the current document’s output path. If no metas +are available, raises an error. If @racket[_suffix] evaluates to a string or a list of strings, they +are appended verbatim to the end of the hash. + +This ID is used when creating URL fragment links within an article, such as for footnotes and index +entries. As long as the web version of the article is not moved to a new URL, the ID will remain the +same, which ensures deep links using the ID don’t break. The ID also ensures each article’s internal +links will be unique, so that links do not collide when multiple articles are being shown on +a single HTML page. @section{Metas and @code{txexpr}s} @defproc[(maybe-attr [key symbol?] [attrs txexpr-attrs?] [missing-expr any/c ""]) any/c] Index: code-docs/pollen.scrbl ================================================================== --- code-docs/pollen.scrbl +++ code-docs/pollen.scrbl @@ -180,10 +180,29 @@ ◊dialogue{ ◊say["Tavi"]{You also write fiction, or you used to. Do you still?} ◊say["Lorde"]{The thing is, when I write now, it comes out as songs.} } }| + +@defproc[(index [heading string?] [elements xexpr?] ...) txexpr?] + +Creates an entry in the keyword index under @racket[_heading] that points back to this spot in the +document. If @racket[_elements] is not empty, the web edition of the document will use it as the +contents of an understated hyperlink to back to @racket[_heading] in the keyword index. + +The example below will create two index entries, one under the heading “compassion” and one under +the heading “cats”: + +@codeblock|{ + #lang pollen + + “I have a theory which I suspect is rather immoral,” Smiley + went on, more lightly. “Each of us has only a quantum of + ◊index["compassion"]{compassion}. That if we lavish our concern + on every stray ◊index["cats"] cat we never get to the centre of + things. What do you think of it?” +}| @defproc[(note [#:date date-str non-empty-string?] [#:author author string? ""] [#:author-url author-url string? ""] [#:disposition disp-str string? ""]) txexpr?] Index: crystalize.rkt ================================================================== --- crystalize.rkt +++ crystalize.rkt @@ -101,22 +101,31 @@ '(pagenode title published noun_plural noun_singular)) + +(define table_keywordindex-fields + '(entry + subentry + pagenode + anchor)) (define table_articles (make-table-schema "articles" table_articles-fields)) (define table_notes (make-table-schema "notes" table_notes-fields #:primary-key-cols '(pagenode note_id))) (define table_series (make-table-schema "series" table_series-fields)) +(define table_keywordindex (make-table-schema "keywordindex" + table_keywordindex-fields + #:primary-key-cols '(pagenode anchor))) ;; ~~~ Provided functions: Initializing; Saving posts and notes ;; Initialize the database connection, creating the database if it doesn’t ;; exist, and executing the table schema queries ;; (define (spell-of-summoning!) - (init-db! DBFILE table_articles table_notes table_series)) + (init-db! DBFILE table_articles table_notes table_series table_keywordindex)) ;; Save an article and its notes (if any) to the database, and return the ;; rendered HTML of the complete article. ;; (define (crystalize-article! pagenode doc) @@ -137,10 +146,12 @@ [series-node (series-pagenode)] [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) ;; Values must come in the order defined in table_article_fields (define article-record (list (symbol->string pagenode) title-plain @@ -336,10 +347,32 @@ (html$-note-in-article note-id note-date content-html author author-url)) (define (article-plain-title pagenode) (query-value (sqltools:dbc) "SELECT `title_plain` FROM `articles` WHERE `pagenode` = ?1" (symbol->string pagenode))) +;; ~~~ Keyword Index Entries ~~~ + +;; (private) Save any index entries in doc to the cache +(define (crystalize-index-entries! pagenode doc) + (define (index-entry? tx) + (and (txexpr? tx) + (string=? "index-link" (attr-ref tx 'class "")) ; see definition of html-index + (attr-ref tx 'data-index-entry #f))) + (define-values (_ entries) (splitf-txexpr doc index-entry?)) + + ; Naive idempotence: delete and re-insert all index entries every time doc is rendered. + (query! "DELETE FROM `keywordindex` WHERE `pagenode` = ?1" (symbol->string pagenode)) + + (unless (null? entries) + (define entry-rows + (for/list ([entry-tx (in-list entries)]) + (list (attr-ref entry-tx 'data-index-entry) + "" ; subentries not yet implemented + (symbol->string pagenode) + (attr-ref entry-tx 'id)))) + (query! (make-insert-rows-query "keywordindex" table_keywordindex-fields entry-rows)))) + ;; ~~~ Series ~~~ ;; Preloads the SQLite cache with info about each series. ;; I may not actually need this but I’m leaving it for now. (define (preheat-series!) Index: dust.rkt ================================================================== --- dust.rkt +++ dust.rkt @@ -24,10 +24,11 @@ (require pollen/core pollen/pagetree pollen/setup pollen/file net/uri-codec + file/sha1 gregor txexpr racket/list racket/system racket/string) @@ -35,10 +36,11 @@ ;; Provides common helper functions used throughout the project (provide maybe-meta ; Select from (current-metas) or default value ("") if not available maybe-attr ; Return an attribute’s value or a default ("") if not available here-output-path + here-id series-noun ; Retrieve noun-singular from current 'series meta, or "" series-title ; Retrieve title of series in current 'series meta, or "" series-pagenode invalidate-series make-tag-predicate @@ -61,11 +63,10 @@ (define series-folder "series") (define articles-folder "articles") (define (default-title body-txprs) (format "“~a…”" (first-words body-txprs 5))) - (define (maybe-meta m [missing ""]) (cond [(current-metas) (or (select-from-metas m (current-metas)) missing)] [else missing])) @@ -98,10 +99,18 @@ (define (series-title) (define series-pnode (series-pagenode)) (case series-pnode ['|| ""] ; no series specified [else (or (select-from-metas 'title series-pnode) "")])) + +;; Generates a short ID for the current article +(define (here-id [suffix #f]) + (define here-hash + (substring (bytes->hex-string (sha1-bytes (path->bytes (here-output-path)))) 0 8)) + (cond [(list? suffix) (apply string-append here-hash suffix)] + [(string? suffix) (string-append here-hash suffix)] + [else here-hash])) ;; “Touches” the last-modified date on the current article’s series, if there is one (define (invalidate-series) (define series-name (maybe-meta 'series #f)) Index: pollen.rkt ================================================================== --- pollen.rkt +++ pollen.rkt @@ -112,10 +112,11 @@ (poly-branch-tag section) (poly-branch-tag subsection) (poly-branch-tag code) (poly-branch-tag dialogue) (poly-branch-tag say) +(poly-branch-tag index) (poly-branch-kwargs-tag blockcode) (poly-branch-kwargs-tag verse) ; [#:title ""] [#:italic "no"] (poly-branch-tag link) (poly-branch-tag url) Index: tags-html.rkt ================================================================== --- tags-html.rkt +++ tags-html.rkt @@ -26,10 +26,11 @@ (require (for-syntax racket/base racket/syntax)) (require racket/list racket/function pollen/decode pollen/tag + net/uri-codec txexpr "dust.rkt") (provide html-fn html-fndef) @@ -74,10 +75,11 @@ html-newthought html-smallcaps html-center html-block html-blockcode + html-index html-dialogue html-say html-verse html-link html-url @@ -113,10 +115,16 @@ (define (html-blockcode attrs elems) (define file (or (assoc 'filename attrs) "")) (define codeblock `(pre [[class "code"]] (code ,@elems))) (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))))] + [data-index-entry ,(car elems)] + [class "index-link"]] + ,@(cdr elems))) (define (html-say . elems) `(@ (dt ,(car elems) (span [[class "x"]] ": ")) (dd ,@(cdr elems)))) (define (html-verse attrs elems)