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]) - ◊@{ - The Local Yarn◊when/splice[title]{: ◊title} - - - - }) - -(define (html-page-top) - ◊@{
-
- -

The Local Yarn

-
}) - -(define (html-article-header) - (define title (select-from-metas 'title (current-metas))) + pollen/template + racket/string + txexpr + openssl/sha1 + "dust.rkt") + +(provide html$-page-head + html$-page-body-open + html$-article-open + html$-article-close + html$-page-body-close + html$-note-title + html$-note-contents + html$-note-listing-full + html$-note-in-article + html$-notes-section) + +(define (html$-page-head [title #f]) + (define title-part (if title (format ": ~a" title) "")) + ◊string-append{ + The Local Yarn◊|title| + + + + }) + +(define (html$-page-body-open) + ◊string-append{
+
+ +

The Local Yarn

+
}) + +(define (html$-article-open title-html-flow published) (define published (select-from-metas 'published (current-metas))) - (cond - [title - ◊string-append{
-

◊|title|

-

- -

-
}] - [else - ◊string-append{
-

- -

-
}])) - -(define (html-article-footer) + (cond + [title-html-flow + ◊string-append{
+

◊|title-html-flow|

+

+ +

+
}] + [else + ◊string-append{
+

+ +

+
}])) + +(define (html$-article-close) ◊string-append{
-
(Part of ‘Talking About Poetry’. Once I threw a mudball at a birdhouse. I’m not exactly proud of it, though.)
-
}) +
(Part of ‘Talking About Poetry’. Once I threw a mudball at a birdhouse. I’m not exactly proud of it, though.)
+
}) + + +(define (html$-page-body-close) + ◊string-append{
By Joel Dueck
+
}) + +;; Notes +;; +(define (html$-note-title author pagenode parent-title) + (define author-part + (cond [(and (non-empty-string? author) + (not (string-ci=? author default-authorname))) + (format "A note from ~a, " author)] + [else ""])) + (define article-part + (format "Re: ~a" + pagenode + parent-title)) + (string-append author-part article-part)) + +(define (html$-note-contents disposition-mark elems) + (define-values (first-tag first-attrs first-elems) (txexpr->values (car elems))) + (define disposition + (cond [(non-empty-string? disposition-mark) + `(span [[class "disposition-mark"]] ,disposition-mark)] + [else ""])) + (define body-elems + (cond + [(equal? 'p first-tag) + (cons (txexpr 'p first-attrs (cons disposition first-elems)) (cdr elems))] + [else + (cons disposition elems)])) + (string-append* (map ->html body-elems))) + +(define (html$-note-listing-full pagenode note-id title-html-flow date author author-url contents) + (define author-part + (cond [(non-empty-string? author) + ◊string-append{ +
+ —◊|author| +
}] + [else ◊string-append{ +
+ —◊|default-authorname| +
}])) + + ◊string-append{ +
+

◊|title-html-flow|

+

+ +

+
+
◊|contents|
+ ◊author-part +
+
}) +(define (html$-note-in-article id date author author-url contents) + ◊string-append{ +
+

+

+
+ ◊contents +
+
+ —◊|author| +
+
}) -(define (html-page-bottom) - ◊@{ -
}) +(define (html$-notes-section note-htmls) + ◊string-append{
+

Further Notes

+ ◊(apply string-append note-htmls) +
}) Index: template.html.p ================================================================== --- template.html.p +++ template.html.p @@ -1,13 +1,13 @@ -◊html-head[(select-from-metas 'title here)] +◊html$-page-head[(select-from-metas 'title here)] -◊html-page-top[] +◊html$-page-body-open[] ◊spell-of-summoning![] ◊crystalize-article![here doc] -◊html-page-bottom[] +◊html$-page-body-close[] Index: web-extra/martin.css.pp ================================================================== --- web-extra/martin.css.pp +++ web-extra/martin.css.pp @@ -447,10 +447,43 @@ } section.footnotes ol { margin: ◊x-lineheight[0.5] 0 0 0; } + + /* ******* “Further Notes” added to articles ******** + */ + + div.further-notes { + margin-top: ◊x-lineheight[3]; + } + + div.further-notes>h2 { + font-style: normal; + font-feature-settings: "smcp" on; + border-top: solid 2px ◊color-bodytext; + text-transform: lowercase; + } + + div.note h3 { + margin-top: 0; + font-size: 1rem; + font-weight: normal; + font-style: italic; + } + + div.note-meta { + margin-top: ◊x-lineheight[1]; + font-feature-settings: "smcp" on; + color: #888; + } + + span.disposition-mark { + color: ◊color-xrefmark; + display: inline-block; + width: 1em; + } /* ******* (Mobile first) Journal View styling ******* */ section.article-listing h2 { font-weight: normal; @@ -632,10 +665,25 @@ section.entry-content figure figcaption { grid-area: margin; text-align: right; align-self: end; } + + /* ******* (Grid support) “Further Notes” added to articles ******* + */ + + div.further-notes>h2 { + width: calc(100% + 8rem); + margin-left: -8rem; + } + + div.note h3 { + float: left; + margin-left: -8rem; + width: 7rem; + text-align: right; + } /* ******* (Grid support) Journal View styling ******* */ section.article-listing {