Index: crystalize.rkt ================================================================== --- crystalize.rkt +++ crystalize.rkt @@ -34,13 +34,14 @@ (require pollen/setup pollen/core pollen/template pollen/pagetree racket/string + txexpr "sqlite-tools.rkt" "template-html.rkt" - "dates.rkt") + "dust.rkt") ;; ~~~ Provides ~~~ (provide spell-of-summoning! crystalize-article!) @@ -47,77 +48,174 @@ ;; ~~~ Private use ~~~ (define DBFILE (build-path (current-project-root) "vitreous.sqlite")) +;; Since the DB exists to serve as a high-speed cache, the tables are constructed so that +;; the most commonly needed data can be grabbed quickly with extremely simple queries. In +;; the even that you want to do something fancy and custom rather than using the pre-cooked +;; HTML, enough info is provided in the other columns to allow you to do so. +;; (define table_articles-fields '(pagenode - title + title_plain + title_html_flow published updated - doc_html author conceal series_pagenode noun_singular - note_count)) + note_count + doc_html + disposition + disposition_note_id + listing_full_html ; Contains full content in default HTML format, but without notes + listing_excerpt_html ; Not used for now + listing_short_html)) ; Date and title only (define table_notes-fields '(pagenode - note-id - heading + note_id + title_html_flow author + author_url date - note_html)) + disposition + content_html + listing_full_html + listing_excerpt_html ; Not used for now + listing_short_html)) (define table_series-fields '(pagenode title published noun_plural noun_singular)) (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_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 (optional-meta m) - (or (select-from-metas m (current-metas)) "")) - -(define (series-noun) - (define series-pagenode (->pagenode (or (select-from-metas 'series (current-metas)) ""))) - (case series-pagenode - ['|| ""] ; no series specified - [else (or (select-from-metas 'noun-singular series-pagenode) "")])) +(define (doc->body/notes doc) + (define (is-note? tx) (and (txexpr? tx) (equal? 'note (get-tag tx)))) + (splitf-txexpr doc is-note?)) ;; ~~~ 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)) -;; Save an article (using current-doc and current-metas) and its notes (if any) -;; to the database, and return the rendered HTML. +;; 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) - (define header (html-article-header)) - (define footer (html-article-footer)) - (define body (->html (cdr doc))) - ;; TK: store notes separately - - (define saving-query (make-insert/replace-query 'articles table_articles-fields)) - (query! saving-query - (symbol->string pagenode) - (optional-meta 'title) - (select-from-metas 'published (current-metas)) - (optional-meta 'updated) - (string-append header body footer) - (optional-meta 'author) - (optional-meta 'conceal) - (optional-meta 'series) - (series-noun) - 0) ; note_count - - `(@ ,header ,body ,footer)) - + (define pubdate (select-from-metas 'published (current-metas))) + (define-values (body-txpr note-txprs) (doc->body/notes doc)) + (define doc-html (->html (cdr body-txpr))) + + (define-values (disposition disp-note-id) + (notes->last-disposition-values note-txprs)) + (define-values (title-plain title-html-flow) + (make-article-titles (maybe-meta 'title (default-title pubdate)) (report disposition))) + (define header (html$-article-open title-html-flow pubdate)) + (define footer (html$-article-close)) + + (define notes-section-html (crystalize-notes! pagenode title-plain note-txprs)) + + ;; Values must come in the order defined in table_article_fields + (define article-record + (list (symbol->string pagenode) + title-plain + title-html-flow + pubdate + (maybe-meta 'updated) + (maybe-meta 'author default-authorname) + (maybe-meta 'conceal) + (maybe-meta 'series) + (maybe-meta 'noun (series-noun)) + (length note-txprs) + doc-html + disposition + disp-note-id + (string-append header doc-html footer) + "" ; listing_excerpt_html: Not yet used + "")) ; 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}) + +(define (make-article-titles title-val disposition) + (define disposition-part + (cond [(non-empty-string? disposition) + (define-values (mark _) (disposition-values disposition)) + (format "~a" mark)] + [else ""])) + + (cond [(txexpr? title-val) + (values (apply string-append (tx-strs title-val)) + (string-append (->html title-val) disposition-part))] + [else (values title-val (string-append title-val disposition-part))])) + +(define (crystalize-notes! pagenode parent-title note-txprs) + (define (crystalizer note-tx) + (crystalize-note! note-tx (symbol->string pagenode) parent-title)) + + (cond [((length note-txprs) . > . 0) + (define notes-html (map crystalizer note-txprs)) + (html$-notes-section notes-html)] + [else ""])) + +(define (crystalize-note! note-tx pagenode parent-title-plain) + (define-values (_ attrs elems) (txexpr->values note-tx)) + (define disposition-attr (maybe-attr 'disposition attrs)) + (define note-date (maybe-attr 'date attrs)) + + ;; Check required attributes + (unless (non-empty-string? note-date) + (raise-arguments-error 'note "required attr missing: date" "attrs" attrs)) + (unless (or (string=? "" disposition-attr) + (and ((length (string-split disposition-attr)) . >= . 2))) + (raise-arguments-error 'note + "must be in format \"[symbol] [past-tense-verb]\"" + "disposition attr" + disposition-attr)) + + ;; Parse out remaining columns + (define author (maybe-attr 'author attrs)) + (define note-id (build-note-id note-tx)) + (define title-html-flow (html$-note-title author 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 author author-url content-html)) + + (define note-record + (list pagenode + note-id + title-html-flow + author + author-url + note-date + disposition-attr + content-html + listing-full-html + "" ; listing_excerpt_html: Not used for now + "")) ; listing_short_html: Not used for now + + ;; save to db + (define save-note-query + (format (string-append "INSERT OR REPLACE INTO `notes` (`rowid`, ~a) " + "VALUES ((SELECT `rowid` FROM `notes` WHERE `pagenode` = ?1" + " AND `note_id` = ?2), ~a)") + (list->sql-fields table_notes-fields) + (list->sql-parameters table_notes-fields))) + (apply query! save-note-query note-record) + + ;; return html$ of note + (html$-note-in-article note-id note-date author author-url content-html)) DELETED dates.rkt Index: dates.rkt ================================================================== --- dates.rkt +++ dates.rkt @@ -1,50 +0,0 @@ -#lang racket/base - -;; Copyright (c) 2018 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 -;; ------------------------------------------------------------------------- - -;; Convenience functions for YYYY-MM-DD date strings - -(require gregor - racket/string) - -(provide (all-defined-out)) - -;; These functions ignore everything after the first space! -(define (ymd->dateformat ymd-string dateformat) - (~t (iso8601->date (car (string-split ymd-string))) dateformat)) - -(define (ymd->english ymd-string) - (ymd->dateformat ymd-string "MMMM d, yyyy")) - -(module+ test - (require rackunit) - (check-equal? (ymd->english "2018-08-12") "August 12, 2018") - (check-equal? (ymd->dateformat "2018-08-12" "d MMM YYYY") "12 Aug 2018") - - ;; How we handle weird input - (check-equal? (ymd->english "2018-08-12 everything after 1st space ignored") "August 12, 2018") - (check-equal? (ymd->english "2018-08 omitting the day") "August 1, 2018") - (check-equal? (ymd->english "2018 omitting month and day") "January 1, 2018") - (check-equal? (ymd->dateformat "2018-08-12" "123") "123") - - ;; Stuff we just don't handle - (check-exn exn:gregor:parse? (lambda () (ymd->english "2018-xyz")))) ADDED dust.rkt Index: dust.rkt ================================================================== --- dust.rkt +++ dust.rkt @@ -0,0 +1,138 @@ +#lang racket/base + +;; Copyright (c) 2018 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 +;; ------------------------------------------------------------------------- + +(require pollen/core + pollen/pagetree + net/uri-codec + gregor + txexpr + racket/list + racket/string) + +;; 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 + series-noun ; Retrieve noun-singular from current 'series meta, or "" + attr-present? ; Test if an attribute is present + disposition-values + ymd->english + ymd->dateformat + default-authorname + default-title + tx-strs + build-note-id + notes->last-disposition-values + ) + +(define default-authorname "Joel Dueck") + +(define (default-title date) + (format "Entry of ~a" (ymd->dateformat date "d MMM YYYY"))) + +(define (maybe-meta m [missing ""]) + (or (select-from-metas m (current-metas)) missing)) + +(define (series-noun) + (define series-pagenode (->pagenode (or (select-from-metas 'series (current-metas)) ""))) + (case series-pagenode + ['|| ""] ; no series specified + [else (or (select-from-metas 'noun-singular series-pagenode) "")])) + +(define (attr-present? name attrs) + (for/or ([attr-pair (in-list attrs)]) + (equal? name (car attr-pair)))) + +(define (maybe-attr name attrs [missing ""]) + (define result (assoc name attrs)) + (cond + [(pair? result) (cadr result)] + [else missing])) + +(define (tx-strs xpr) + (cond + [(txexpr? xpr) (apply string-append (map tx-strs (get-elements xpr)))] + [(string? xpr) xpr] + [else ""])) + +(module+ test + (require rackunit) + (define test-metas (hash 'name "Fiver" 'size "Small")) + (define test-attrs '([name "Hazel"] [rank "Chief"])) + + (parameterize ([current-metas test-metas]) + (check-equal? (maybe-meta 'name) "Fiver") ; present meta + (check-equal? (maybe-meta 'age) "") ; missing meta + (check-equal? (maybe-meta 'age 2) 2)) ; alternate default value + + (check-equal? (attr-present? 'name test-attrs) #t) + (check-equal? (attr-present? 'dingus test-attrs) #f) + (check-equal? (maybe-attr 'rank test-attrs) "Chief") + (check-equal? (maybe-attr 'dingus test-attrs) "") + (check-equal? (maybe-attr 'dingus test-attrs "zippy") "zippy")) + +;; Convert, e.g., "* thoroughly recanted" into (values "*" "thoroughly recanted") +(define (disposition-values str) + (cond [(string=? "" str) (values "" "")] + [else (let ([splut (string-split str)]) + (values (car splut) (string-join (cdr splut))))])) + +;; The format of a note’s ID is “HTML-driven” (used as an anchor link) but is included +;; here since it also serves as a primary key in the DB. +(define (build-note-id txpr) + (string-append "#" + (maybe-attr 'date (get-attrs txpr)) + "_" + (uri-encode (maybe-attr 'author (get-attrs txpr) default-authorname)))) + +;; Extract the last disposition (if any), and the ID of the disposing note, out of a list of notes +(define (notes->last-disposition-values txprs) + (define (contains-disposition? tx) (attr-present? 'disposition (get-attrs tx))) + (define disp-notes (filter contains-disposition? txprs)) + (cond [(not (empty? disp-notes)) + (define latest-disposition-note (last disp-notes)) + (values (attr-ref latest-disposition-note 'disposition) + (build-note-id latest-disposition-note))] + [else (values "" "")])) + +;; ~~~ Convenience functions for YYYY-MM-DD date strings ~~~ + +;; These functions ignore everything after the first space in the input! +(define (ymd->dateformat ymd-string dateformat) + (~t (iso8601->date (car (string-split ymd-string))) dateformat)) + +(define (ymd->english ymd-string) + (ymd->dateformat ymd-string "MMMM d, yyyy")) + +(module+ test + (check-equal? (ymd->english "2018-08-12") "August 12, 2018") + (check-equal? (ymd->dateformat "2018-08-12" "d MMM YYYY") "12 Aug 2018") + + ;; How we handle weird input + (check-equal? (ymd->english "2018-08-12 everything after 1st space ignored") "August 12, 2018") + (check-equal? (ymd->english "2018-08 omitting the day") "August 1, 2018") + (check-equal? (ymd->english "2018 omitting month and day") "January 1, 2018") + (check-equal? (ymd->dateformat "2018-08-12" "123") "123") + + ;; Stuff we just don't handle + (check-exn exn:gregor:parse? (lambda () (ymd->english "2018-xyz")))) Index: pollen.rkt ================================================================== --- pollen.rkt +++ pollen.rkt @@ -43,11 +43,11 @@ (provide (all-defined-out)) (define poly-targets '(html)) (define cache-watchlist (map resolve-module-path '("tags-html.rkt" "template-html.rkt" - "dates.rkt" + "dust.rkt" "crystalize.rkt")))) ;; Macro for defining tag functions that automatically branch based on the ;; current output format and the list of poly-targets in the setup module. ;; Index: tags-html.rkt ================================================================== --- tags-html.rkt +++ tags-html.rkt @@ -26,11 +26,12 @@ (require (for-syntax racket/base racket/syntax)) (require racket/list racket/function pollen/decode pollen/tag - txexpr) + txexpr + "dust.rkt") (provide html-fn html-fndef) ;; Customized paragraph decoder replaces single newlines within paragraphs Index: template-html.rkt ================================================================== --- template-html.rkt +++ template-html.rkt @@ -21,51 +21,135 @@ ;; https://joeldueck.com ;; ------------------------------------------------------------------------- ;; Provides functions for displaying content in HTML templates. (require pollen/core - "dates.rkt") - -(provide (all-defined-out)) - -(define (html-head [title #f]) - ◊@{
-
+
+