@@ -33,10 +33,11 @@ (require pollen/setup pollen/core pollen/template racket/string + racket/function txexpr db/base "sqlite-tools.rkt" "snippets-html.rkt" "dust.rkt") @@ -44,10 +45,17 @@ ;; ~~~ Provides ~~~ (provide spell-of-summoning! crystalize-article! article-plain-title + list/articles + 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")) @@ -83,10 +91,11 @@ author author_url date disposition content_html + series_pagenode listing_full_html listing_excerpt_html ; Not used for now listing_short_html)) (define table_series-fields @@ -93,22 +102,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) @@ -118,21 +136,23 @@ (body-txpr note-txprs) (splitf-txexpr doc2 (make-tag-predicate 'note))) (define-values (disposition disp-note-id) (notes->last-disposition-values note-txprs)) (let* ([pubdate (select-from-metas 'published (current-metas))] - [doc-html (->html (cdr body-txpr))] + [doc-html (->html body-txpr #:splice? #t)] [title-specified? (not (equal? '() maybe-title))] [title-val (if (not (null? maybe-title)) (car maybe-title) maybe-title)] [title-tx (make-article-title title-val body-txpr disposition disp-note-id)] - [title-html (->html title-tx)] + [title-html (->html title-tx #:splice? #t)] [title-plain (tx-strs title-tx)] [series-node (series-pagenode)] - [header (html$-article-open title-specified? title-tx pubdate)] + [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 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 @@ -148,16 +168,68 @@ doc-html disposition disp-note-id (string-append header doc-html footer) "" ; listing_excerpt_html: Not yet used - "")) ; listing_short_html: Not yet used + (html$-article-listing-short pagenode pubdate title-plain))) ; listing_short_html: Not yet used (apply query! (make-insert/replace-query 'articles table_articles-fields) article-record) (string-append header doc-html notes-section-html footer))) +;; ~~~ Retrieve listings of articles and notes ~~~ +;; ~~~ (Mainly for use on Series pages ~~~ + +;; (private) Create a WHERE clause matching a single series or list of series +(define (where/series s) + (cond [(list? s) + (let ([series (map (curry (format "~a/~a.html" series-folder)) s)]) + (format "WHERE `series_pagenode` IN ~a" (list->sql-values series)))] + [(string? s) + (format "WHERE `series_pagenode` IS \"~a/~a.html\"" series-folder s)] + [(equal? s #t) + (format "WHERE `series_pagenode` IS \"~a\"" (here-output-path))] + [else ""])) + +;; Return a combined list of articles and notes sorted by date +(define (list/articles+notes type #:series [s #t] #:limit [limit -1] [order "DESC"]) + (define select #<<@@@@@ + SELECT `~a` FROM + (SELECT `~a`, `published` FROM `articles` + UNION SELECT + `~a`,`date` AS `published` FROM `notes` + ~a ORDER BY `published` ~a LIMIT ~a) +@@@@@ + ) + (query-list (sqltools:dbc) (format select type type type (where/series s) order limit))) + +;; Return a list of articles only, sorted by date +(define (list/articles type #:series [s #t] #:limit [limit -1] [order "DESC"]) + (define select "SELECT `~a` FROM `articles` ~a ORDER BY `published` ~a LIMIT ~a") + (query-list (sqltools:dbc) (format select type (where/series s) order limit))) + +;; ~~~~ +;; Return cached HTML of articles and/or notes, fenced within a style txexpr to prevent it being +;; escaped by ->html. See also: definition of `unfence` + +(define (listing<>-short/articles #:series [s #t] #:limit [limit -1] [order "DESC"]) + `(style "")) + +(define (listing<>-full/articles #:series [s #t] #:limit [limit -1] [order "DESC"]) + `(style ,@(list/articles "listing_full_html" #:series s #:limit limit order))) + +;; Return a combined list of articles and notes (“full content” version) sorted by date +(define (listing<>-full/articles+notes #:series [s #t] #:limit [limit -1] [order "DESC"]) + `(style ,@(list/articles+notes "listing_full_html" #:series s #:limit limit order))) + +;; Remove "" introduced by using ->html on docs containing output from +;; listing functions +(define (unfence html-str) + (regexp-replace* #px"<[\\/]{0,1}style>" html-str "")) + ;; ~~~ Article-related helper functions ~~~ ;; ;; Return a title txexpr for the current article, constructing a default if no title text was specified. (define (make-article-title supplied-title body-tx disposition disp-note-id) @@ -177,11 +249,11 @@ (define (make-article-footertext pagenode series disposition disp-note-id note-count) (define s-title (series-title)) (define s-noun (series-noun)) (define series-part (cond [(non-empty-string? s-title) - (format "This is ~a, part of ‘~a’." + (format "This is ~a, part of ‘~a’." s-noun series s-title)] [else ""])) (define disp-part @@ -200,12 +272,12 @@ [(and (note-count . > . 0) (string=? disposition "")) (format "There is a note appended." pagenode)] [else ""])) - (cond [(andmap non-empty-string? (list series-part disp-part notes-part)) - (format "~a ~a ~a" series-part disp-part notes-part)] + (cond [(ormap non-empty-string? (list series-part disp-part notes-part)) + (string-join (list series-part disp-part notes-part))] [else ""])) ;; ~~~ Notes ~~~ @@ -238,13 +310,13 @@ "must be in format \"[symbol] [past-tense-verb]\"" "disposition attr" disposition-attr)) ;; Parse out remaining columns - (define author (maybe-attr 'author attrs)) + (define author (maybe-attr 'author attrs default-authorname)) (define note-id (build-note-id note-tx)) - (define title-html-flow (html$-note-title author pagenode parent-title-plain)) + (define title-html-flow (html$-note-title pagenode parent-title-plain)) (define author-url (maybe-attr 'author-url attrs)) (define-values (disp-mark disp-verb) (disposition-values disposition-attr)) (define content-html (html$-note-contents disp-mark (get-elements note-tx))) (define listing-full-html (html$-note-listing-full pagenode note-id title-html-flow note-date content-html author author-url)) @@ -256,10 +328,11 @@ author author-url note-date disposition-attr content-html + (symbol->string (series-pagenode)) listing-full-html "" ; listing_excerpt_html: Not used for now "")) ; listing_short_html: Not used for now ;; save to db @@ -275,10 +348,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!)