Index: .fossil-settings/ignore-glob ================================================================== --- .fossil-settings/ignore-glob +++ .fossil-settings/ignore-glob @@ -1,21 +1,20 @@ *compiled/* *.woff* x-*/* *.*~ -web-extra/martin.css +web/martin.css scribbled/* -code-docs/*.css -code-docs/*.js +yarn-doc/*.css +yarn-doc/*.js */images/* *.db *.sqlite *.pdf -*.ltx *.html *.out *.ltx *.aux *.log *.xml *.toc *.mark DELETED cache.rkt Index: cache.rkt ================================================================== --- cache.rkt +++ cache.rkt @@ -1,209 +0,0 @@ -#lang racket/base - -; SPDX-License-Identifier: BlueOak-1.0.0 -; This file is licensed under the Blue Oak Model License 1.0.0. - -(require deta - db/base - db/sqlite3 - threading - pollen/setup - racket/match - "dust.rkt" - (except-in pollen/core select)) - -(provide init-cache-db! - cache-conn ; The most eligible bachelor in Neo Yokyo - (schema-out cache:article) - (schema-out cache:note) - (schema-out cache:index-entry) - (schema-out listing) - delete-article! - delete-notes! - delete-index-entries! - save-cache-things! - articles - articles+notes - listing-htmls - fenced-listing - unfence) - -;; Cache DB and Schemas - -(define DBFILE (build-path (current-project-root) "vitreous.sqlite")) -(define cache-conn (make-parameter (sqlite3-connect #:database DBFILE #:mode 'create))) - -(define-schema cache:article #:table "articles" - ([id id/f #:primary-key #:auto-increment] - [page symbol/f] - [title-plain string/f #:nullable] - [title-html-flow string/f #:nullable] - [title-specified? boolean/f #:nullable] - [published string/f #:nullable] - [updated string/f #:nullable] - [author string/f #:nullable] - [conceal string/f] - [series-page symbol/f #:nullable] - [noun-singular string/f #:nullable] - [note-count integer/f #:nullable] - [content-html string/f #:nullable] - [disposition string/f #:nullable] - [disp-html-anchor string/f #:nullable] - [listing-full-html string/f #:nullable] ; full content but without notes - [listing-excerpt-html string/f #:nullable] ; Not used for now - [listing-short-html string/f #:nullable])) ; Date and title only - -(define-schema cache:note #:table "notes" - ([id id/f #:primary-key #:auto-increment] - [page symbol/f] - [html-anchor string/f] - [title-html-flow string/f] ; No block-level HTML elements - [title-plain string/f] - [author string/f] - [author-url string/f] - [published string/f] - [disposition string/f] - [content-html string/f] - [series-page symbol/f] - [conceal string/f] - [listing-full-html string/f] - [listing-excerpt-html string/f] ; Not used for now - [listing-short-html string/f])) ; Date and title only - -(define-schema cache:index-entry #:table "index_entries" - ([id id/f #:primary-key #:auto-increment] - [entry string/f] - [subentry string/f] - [page symbol/f] - [html-anchor string/f])) - -(define-schema listing - #:virtual - ([path string/f] - [title string/f] - [author string/f] - [published string/f] - [updated string/f] - [html string/f])) - -(define (init-cache-db!) - (create-table! (cache-conn) 'cache:article) - (create-table! (cache-conn) 'cache:note) - (create-table! (cache-conn) 'cache:index-entry)) - -(define (delete-article! page) - (query-exec (cache-conn) - (~> (from cache:article #:as a) - (where (= a.page ,(format "~a" page))) - delete))) - -(define (delete-notes! page) - (query-exec (cache-conn) - (~> (from cache:note #:as n) - (where (= n.page ,(format "~a" page))) - delete))) - -(define (delete-index-entries! page) - (query-exec (cache-conn) - (~> (from cache:index-entry #:as e) - (where (= e.page ,(format "~a" page))) - delete))) - -(define (save-cache-things! es) - (void (apply insert! (cache-conn) es))) - -;; -;; ~~~ Fetching articles and notes ~~~ -;; - -;; (Private use) Conveniece function for the WHERE `series-page` clause -(define (where-series q s) - (define (s->p x) (format "~a/~a.html" series-folder x)) - (match s - [(list series ...) - (where q (in a.series-page ,(map s->p series)))] ; WHERE series-page IN (item1 ...) - [(or (? string? series) (? symbol? series)) - (where q (= a.series-page ,(s->p series)))] ; WHERE series-page = "item" - [#t - (where q (like a.series-page ,(format "%~a" (here-output-path))))] - [_ q])) - -;; (Private use) Convenience for the WHERE `conceal` NOT LIKE clause -(define (where-not-concealed q) - (define base-clause (where q (not (like a.conceal "%all%")))) - (match (listing-context) - ["" base-clause] - [(var context) (where base-clause (not (like a.conceal ,(format "%~a%" context))))])) - -;; Needed to "parameterize" column names -;; see https://github.com/Bogdanp/deta/issues/14#issuecomment-573344928 -(require (prefix-in ast: deta/private/ast)) - -;; Builds a query to fetch articles -(define (articles type #:series [s #t] #:limit [lim -1] #:order [ord 'desc]) - (define html-field - (match type - ['content "content_html"] - [_ (format "listing_~a_html" type)])) - (~> (from cache:article #:as a) - (select (as a.page path) - (as a.title-plain title) - a.author - a.published - a.updated - (fragment (ast:as (ast:qualified "a" html-field) "html"))) - (where-series s) - (where-not-concealed) - (limit ,lim) - (order-by ([a.published ,ord])) - (project-onto listing-schema))) - -;; Builds a query that returns articles and notes intermingled chronologically -(define (articles+notes type #:series [s #t] #:limit [lim -1] #:order [ord 'desc]) - (define html-field - (match type - ['content "content_html"] - [_ (format "listing_~a_html" type)])) - (~> (from (subquery - (~> (from cache:article #:as A) - (select - (as A.page path) - (as A.title-plain title) - A.author - A.published - A.updated - (fragment (ast:as (ast:qualified "A" html-field) "html")) - A.series-page - A.conceal) - (union - (~> (from cache:note #:as N) - (select - (as (array-concat N.page "#" N.html-anchor) path) - (as N.title-plain title) - N.author - N.published - (as "" updated) - (fragment (ast:as (ast:qualified "N" html-field) "html")) - N.series-page - N.conceal))))) - #:as a) - (where-series s) - (where-not-concealed) - (limit ,lim) - (order-by ([a.published ,ord])) - (project-onto listing-schema))) - -;; Get all the a list of the HTML all the results in a query -(define (listing-htmls list-query) - (for/list ([l (in-entities (cache-conn) list-query)]) - (listing-html l))) - -;; 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 (fenced-listing q) - `(style ,@(listing-htmls q))) - -;; Remove "" introduced by using ->html on docs containing output from -;; listing functions -(define (unfence html-str) - (regexp-replace* #px"<[\\/]{0,1}style>" html-str "")) DELETED code-docs/cache.scrbl Index: code-docs/cache.scrbl ================================================================== --- code-docs/cache.scrbl +++ code-docs/cache.scrbl @@ -1,246 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt" scribble/example) - -@(require (for-label deta - db - racket/base - racket/contract - sugar/coerce - pollen/template - "../dust.rkt" - "../crystalize.rkt" - "../cache.rkt")) - -@(define example-eval (make-base-eval)) -@(example-eval '(require "cache.rkt" txexpr)) - -@title[#:tag "cache-rkt"]{Cache} - -@defmodule["cache.rkt" #:packages ()] - -In this project there are several places – the blog, the footer on each page, the RSS feed, series -pages — where data from an amorphous group of Pollen documents is needed. This is what the cache is -for. - -This module defines and provides the schema and database connection to the SQLite cache, and some -functions for retrieving records from the cache. Use these when you need quick access to pre-cooked -HTML. - -@section{Cache database} - -@defparam[cache-conn conn connection?]{ -The database connection. -} - -@defproc[(init-cache-db!) void?]{ - -Creates and initializes the SQLite database cache file (named @filepath{vitreous.sqlite} and located -in the project root folder) by running queries to create tables in the database if they do not -exist. - -} - -@section{Retrieving cached data} - -Some of this looks a little wacky, but it’s a case of putting a little extra complextity into the -back end to make things simple on the front end. These functions are most commonly used inside the -@emph{body} of a Pollen document (i.e., series pages). - -@filebox["series/my-series.poly.pm" -@codeblock|{ -#lang pollen - -◊title{My New Series} - -...some other content - -◊fenced-listing[(articles+notes 'excerpt #:order 'asc)] -}| -] - -@defproc[(fenced-listing [query query?]) txexpr?]{ - -Fetches a the HTML strings from the SQLite cache and returns a @racket['style] tagged X-expression -with these strings as its elements. The @racket[_query] will usually be the result of a call to -@racket[articles] or @racket[articles+notes], but can be any custom query that projects onto the -@racket[listing] schema (see @racket[project-onto]). - -The reason for enclosing the results in a @racket['style] txexpr is to prevent the HTML from being -escaped by @racket[->html] in the template. This tag was picked for the job because there will -generally never be a need to include any actual CSS information inside a @tt{"] removed. -The contents of the style tags are left intact. - -Use this in templates with strings returned from @racket[->html] when called on docs that use the -@racket[fenced-listing] tag function. - -} - -@section{Modifying the cache} - -@defproc[(save-cache-things! - [things (listof (or/c cache:article? cache:note? cache:index-entry?))]) void?]{ - -Saves all the @racket[_thing]s to the cache database. - -} - -@deftogether[(@defproc[(delete-article! [page stringish?]) void?] - @defproc[(delete-notes! [page stringish?]) void?])]{ - -Delete a particular article, or all notes for a particular article, respectively. - -} - -@section{Schema} - -The cache database has four tables: @tt{articles}, @tt{notes}, @tt{index_entries} and @tt{series}. -Each of these has a corresponding schema, shown below. In addition, there is a “virtual” schema, -@tt{listing}, for use with queries which may or may not combine articles and notes intermingled. - -The work of picking apart an article’s exported @tt{doc} and @tt{metas} into rows in these tables is -done by @racket[parse-and-cache-article!]. - -The below are shown as @code{struct} forms but are actually defined with deta’s -@racket[define-schema]. Each schema has an associated struct with the same name and a smart -constructor called @tt{make-@emph{id}}. The struct’s “dumb” constructor is hidden so that invalid -entities cannot be created. For every defined field there is an associated functional setter and -updater named @tt{set-@emph{id}-field} and @tt{update-@emph{id}-field}, respectively. - -@defstruct*[cache:article ([id id/f] - [page symbol/f] - [title-plain string/f] - [title-html-flow string/f] - [title-specified boolean/f] - [published string/f] - [updated string/f] - [author string/f] - [conceal string/f] - [series-page string/f] - [noun-singular string/f] - [note-count integer/f] - [content-html string/f] - [disposition string/f] - [disp-html-anchor string/f] - [listing-full-html string/f] - [listing-excerpt-html string/f] - [listing-short-html string/f]) - #:constructor-name make-cache:article]{ - -Table holding cached @tech{article} information. - -When creating a @racket[cache:article] (should you ever need to do so directly, which is unlikely), -the only required fields are @racket[_page], @racket[_title], and @racket[_conceal]. - -} - -@defstruct*[cache:note ([id id/f] - [page symbol/f] - [html-anchor string/f] - [title-html-flow string/f] - [title-plain string/f] - [author string/f] - [author-url string/f] - [published string/f] - [disposition string/f] - [content-html string/f] - [series-page symbol/f] - [conceal string/f] - [listing-full-html string/f] - [listing-excerpt-html string/f] - [listing-short-html string/f]) - #:constructor-name make-cache:note]{ - -Table holding cached information on @tech{notes}. - -} - -@defstruct*[cache:index-entry ([id id/f] - [entry string/f] - [subentry string/f] - [page symbol/f] - [html-anchor string/f]) - #:constructor-name make-cache:index-entry]{ - -Table holding cached information about index entries found in @tech{articles}. - -} - -@defstruct*[listing ([path string/f] - [title string/f] - [author string/f] - [published string/f] - [updated string/f] - [html string/f]) - #:constructor-name make-listing]{ - -This is a “virtual” schema targeted by @racket[articles] and @racket[articles+notes] using deta’s -@racket[project-onto]. It supplies the minimum set of fields needed to build the RSS feed, and which -are common to both articles and notes; most times (e.g., on @tech{series} pages) only the @tt{html} -field is used, via @racket[fenced-listing] or @racket[listing-htmls]. - -} DELETED code-docs/crystalize.scrbl Index: code-docs/crystalize.scrbl ================================================================== --- code-docs/crystalize.scrbl +++ code-docs/crystalize.scrbl @@ -1,61 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt") - -@(require (for-label "../pollen.rkt" - "../crystalize.rkt" - "../cache.rkt" - racket/base - racket/contract - racket/string - txexpr - pollen/core - pollen/pagetree)) - -@title[#:tag "crystalize-rkt"]{Crystalize} - -@defmodule["crystalize.rkt" #:packages ()] - -“Crystalizing” is an extra layer in between docs and templates that destructures the @tt{doc} and -stores it in various pieces in a SQLite cache. Individual articles save chunks of rendered HTML to -the cache when their individual pages are rendered. When pulling together listings of articles in -different contexts that need to be filtered and sorted, a SQL query is much faster than trolling -through the Pollen cache for matching docs and regenerating the HTML. - -@margin-note{These functions are designed to be used within templates, so that the rows in the cache -database for a page are updated right when that web page is rendered.} - -@defproc[(parse-and-cache-article! [pagenode pagenode?] [doc txexpr?]) - (values non-empty-string? non-empty-string?)]{ - -Returns two values: the “plain” title of the article, and a string containing the full HTML of -@racket[_doc], in that order. - -The title is returned separately for use in the HTML @tt{} tag. If the @racket[_doc] doesn’t -specify a title, a provisional title is constructed using @racket[default-title]. - -Privately, it does a lot of other work. The article is analyzed, additional metadata is constructed -and saved to the SQLite cache and saved using @racket[make-cache:article]. If the article specifies -a @racket['series] meta, information about that series is fetched and used in the rendering of the -article. If there are @racket[note]s or @racket[index] tags in the doc, they are parsed and saved -individually to the SQLite cache (using @racket[make-cache:note] and -@racket[make-cache:index-entry]). If any of the notes use the @code{#:disposition} attribute, -information about the disposition is parsed out and used in the rendering of the article. - -} - -@defproc[(cache-index-entries-only! [title string?] [page pagenode?] [doc txexpr?]) void?]{ - -Saves only the @racket[index] entres in @racket[_doc] to the cache database, so that they appear in -the keyword index. - -This function allows pages that are not @tech{articles} to have their own keyword index entries, and -should be used in the templates for such pages. - -As a side effect of calling this function, a minimal @racket[cache:article] is created for the page -with its @racket['conceal] meta set to @racket{all}, to exclude it from any listings. - -} DELETED code-docs/custom.css Index: code-docs/custom.css ================================================================== --- code-docs/custom.css +++ code-docs/custom.css @@ -1,28 +0,0 @@ -.fileblock .SCodeFlow { - padding-top: 0.7em; - margin-top: 0; -} - -.fileblock { - width: 90%; -} - -.fileblock_filetitle{ - background: #eee; - text-align:right; - padding: 0.15em; - border: 1px dotted black; - border-bottom: none; -} - -.terminal, .browser { - margin-bottom: 1em; - padding: 0.5em; - width: 88%; - background: #fcfcfc; - color: rgb(150, 35, 105); -} - -.terminal .SIntrapara, .browser .SIntrapara, .fileblock .SIntrapara { - margin: 0 0 0 0; -} DELETED code-docs/design.scrbl Index: code-docs/design.scrbl ================================================================== --- code-docs/design.scrbl +++ code-docs/design.scrbl @@ -1,172 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt" - racket/runtime-path - (for-label "../pollen.rkt")) - -@(require (for-label racket/base)) - -@title{Basic Notions} - -@section[#:tag "design-goals"]{Design Goals} - -The design of @italic{The Local Yarn} is guided by requirements that have evolved since I started -the site in 1999. I enumerate them here because they explain why the code is necessarily more -complicated than a typical blog: - -@itemlist[ - @item{@bold{The writing will publish to two places from the same source: the web server, and the - bookshelf.} The web server, because it’s a fun, fast way to publish writing and code to the whole - world (you knew that already); but also on bookshelves, because - @ext-link["https://thelocalyarn.com/excursus/secretary/posts/web-books.html"]{a web server is like - a projector}, and I want to be able to turn it off someday and still have something to show for all - my work. Plus, I just like printed books.} - - @item{@bold{Changes are part of the content.} I like to revisit, resurface and amend things I’ve - written before. Views change, new ideas come along. In a typical blog the focus is always at - whatever’s happening at the head of the time stream; an addendum to an older post is, for all - practical purposes, invisible and nearly useless. I want every published edit to an article to be - findable and linkable. I want addenda to be extremely visible. These addenda should also be able to - mark major shifts in the author’s own perspective on what they originally wrote.} - - @item{@bold{The system will gracefully accomodate experimentation.} I should be able to write - fiction, poetry, opinion pieces, minor observations or collections, or anything else, and have or - create a good home for it here.} - - @item{@bold{Everything produced here should look good.}} - - @item{@bold{Reward exploration without disorienting the reader.} Draw connections between related - thoughts using typographic conventions and organizational devices that would be familiar to - a reader of books. Where dissimilar writings appear together, place signals that help the reader - understand what they are looking at, switch contexts, and find more if they wish.} - - @item{@bold{Everything is produced, and reproducible, by an automatable process.} No clicking or - tapping around in GUI apps to publish web pages and books.} - - ] - -@section{Names for things and how they fit together} - -The Local Yarn is mostly comprised of @tech{articles} (individual writings) which may contain -@tech{notes} (addenda by the author or others) and may also be grouped into @tech{series}. These are -similar to a typical blog’s @italic{posts}, @italic{comments} and @italic{categories}, but there are -important differences. - -@subsection{Articles} - -The @deftech{article} is the basic unit of content, like a typical blog post. In the web edition, -each article has its own @tt{.html} file; in print editions, an article may comprise either -a chapter or a part of a chapter, depending on the content. - -An article can start out very small — just a date and a few sentences. @bold{Supplying a title is -optional.} Later, it may grow in any of several directions: @tech{notes} can be added, or a title, or -cross-references to later articles; or it may be added to a series. Or it may just remain the way it -started. - -@subsection{Notes} - -A @deftech{note} is a comment or addendum to an @tech{article} using the @racket[note] tag. It may -be written by the same person who wrote the article, or submitted by a reader. - -@(define-runtime-path diagram-notes "diagram-notes.png") -@centered{@responsive-retina-image[diagram-notes]} - -As shown above, a note appears at the bottom of the article to which it is attached, but it also -appears in the blog and in the RSS feed as a separate piece of content, and is given the same visual -weight as actual articles. - -A note may optionally have a @deftech{disposition} which reflects a change in attitude towards its -parent article. A disposition consists of a @italic{disposition mark} such as an asterisk or dagger, -and a past-tense verb. For example, an author may revisit an opinion piece written years earlier and -add a note describing how their opinion has changed; the tag for this note might include -@racket[#:disposition "* recanted"] as an attribute. This would cause the @tt{*} to be added to the -article’s title, and the phrase “Now considered recanted” to be added to the margin, with a link to -the note. - -@subsubsection{Notes vs. blog “comments”} - -Typical blog comments serve as kind of a temporary discussion spot for a few days or weeks after -a post is published. Commenting on an old post feels useless because the comment is only visible at -the bottom of its parent post, and older posts are never “bumped” back into visibility. - -By contrast, notes on @italic{The Local Yarn} appear as self-contained writings at the top of the -blog and RSS feed as soon as they are published. This “resurfaces” the original article -to which they are attached. This extra visibility also makes them a good tool for the original -author to fill out or update the article. In effect, with notes, each article potentially becomes -its own miniature blog. - -The flip side of this change is that what used to be the “comment section” is no longer allowed to -function as a kind of per-article chat. - -@tabular[#:sep @hspace[1] - #:style 'boxed - #:row-properties '((bottom-border top)) - (list - (list @bold{Typical Blog Comments} @bold{Local Yarn @emph{Notes}}) - (list "Rarely used after a post has aged" - "Commonly used on posts many years old") - (list "Visible only at the bottom of the parent post" - "Included in the main stream of posts and in the RSS feed alongside actual posts") - (list "Invites any and all feedback, from small compliments to lengthy rebuttals" - "Readers invited to treat their responses as submissions to a publication.") - (list "Usually used by readers" - "Usually used by the original author") - (list "Don’t affect the original post" - "May have properties (e.g. disposition) that change the status and -presentation of the original post") - (list "Moderation (if done) is typically binary: approved or not" - "Moderation may take the form of edits and inline responses."))] - -@subsection{Series} - -A @deftech{series} is a grouping of @tech{articles} into a particular order under a descriptive -title. A series may present its own written content alongside the listing of its articles. - -The page for a series can choose how to display its articles: chronologically, or in an arbitrary -order. It can display articles only, or a mixed listing of articles and @tech{notes}, like the blog. -And it can choose to display articles in list form, or as excerpts, or in their entirety. - -A series can specify @italic{nouns} (noun phrases, really) to be applied to its articles. So, for -example, a series of forceful opinion pieces might designate its articles as @emph{naked -aspirations}; the phrase “This is a naked aspiration, part of the series @italic{My Uncensored -Thoughts}” would appear prominently in the margins. Likewise, a time-ordered series of observations -might call its articles “journal entries”. - -It will be easy for any series to become a printed @emph{book}, using the techniques I -demonstrated in -@ext-link["https://thelocalyarn.com/excursus/secretary/posts/web-books.html"]{@italic{The Unbearable -Lightness of Web Pages}}, and in @other-doc['(lib "bookcover/scribblings/bookcover.scrbl")]. - -@subsubsection{Series vs. blog “categories”} - -Typical blogs are not very good at presenting content that may vary a lot in subject, length and -style. The kind of writing I want to experiment with may change a lot from day to day, season to -season, decade to decade. I wanted a single system that could organize extremely varied kinds of -writings and present them in a thoughtful, coherent way, rather than starting a new blog every time -I wanted to try writing a different kind of thing. - -My solution to this was to enrich the idea of “categories”. Rather than being simply labels that you -slap on blog posts, they would be titled collections with their own unique content and way of -presenting articles and notes. In addition, they could pass down certain properties to the posts -they contain, that can be used to give signals to the reader about what they are looking at. - -@tabular[#:sep @hspace[1] - #:style 'boxed - #:row-properties '((bottom-border top)) - (list - (list @bold{Typical Blog Categories/Tags} @bold{Local Yarn @emph{Series}}) - (list "Every article needs to have one" - "Many or most articles won’t have one") - (list "Named with a single word" - "Named with a descriptive title") - (list "Has no content or properties of its own" - "Has its own written content, and properties such as nouns, ordering, etc.") - (list "Broad in scope, few in number" - "Narrow in scope, many in number") - (list "Selected to be relevant for use across the entire lifetime of the site" - "Selected without reference to future creative direction; may be closed after only - a few articles"))] - DELETED code-docs/diagram-notes.png Index: code-docs/diagram-notes.png ================================================================== --- code-docs/diagram-notes.png +++ code-docs/diagram-notes.png cannot compute difference between binary files DELETED code-docs/dust.scrbl Index: code-docs/dust.scrbl ================================================================== --- code-docs/dust.scrbl +++ code-docs/dust.scrbl @@ -1,287 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt" - scribble/example) - -@(require (for-label "../pollen.rkt" - "../dust.rkt" - "../cache.rkt" - "../series-list.rkt" - racket/base - racket/contract - txexpr - sugar/coerce - pollen/tag - pollen/setup - pollen/pagetree - pollen/core)) - -@(define dust-eval (make-base-eval)) -@(dust-eval '(require "dust.rkt" txexpr)) - -@title{Dust} - -@defmodule["dust.rkt" #:packages ()] - -This is where I put constants and helper functions that are needed pretty much everywhere in the -project. In a simpler project these would go in @seclink["pollen-rkt"]{@filepath{pollen.rkt}} but -here I have other modules sitting “behind” that one in the @tt{require} chain. - -@section{Constants} - -@defthing[default-authorname string? #:value "Joel Dueck"] - -Used as the default author name for @code{note}s, and (possibly in the future) for articles -generally. - -@defthing[web-root path-string? #:value "/"] - -Specifies the path between the domain name and the root folder of the website generated by this -project. - -@deftogether[(@defthing[articles-folder path-string? #:value "articles"] - @defthing[series-folder path-string? #:value "series"])] - -The names of the folders that contain the Pollen source documents for Articles and Series -respectively, relative to the project’s document root. - -@defthing[images-folder path-string? #:value "images"] - -The name of the subfolders within @racket[articles-folder] and @racket[series-folder] used for -holding image files. - -@deftogether[(@defproc[(articles-pagetree) pagetree?] - @defproc[(series-pagetree) pagetree?])] - -These are project-wide pagetrees: @racket[articles-pagetree] contains a pagenode for every Pollen -document contained in @racket[articles-folder], and @racket[series-pagetree] contains a pagenode for -every Pollen document in @racket[series-folder]. The pagenodes themselves point to the rendered -@tt{.html} targets of the source documents. - -@deftogether[(@defproc[(here-output-path) path?] - @defproc[(here-source-path) path?])]{ - -Returns the path to the current output or source file, relative to @racket[current-project-root]. If -no metas are available, returns @racket[(string->path ".")]. - -For the output path, this is similar to the @tt{here} variable that Pollen provides, except it is -available outside templates. As to the source path, Pollen provides it via the @racket['here-path] -key in the current metas, but it is a full absolute path, rather then relative to -@racket[current-project-root]. - -} - -@defproc[(checked-in?) boolean?]{ - -Returns @racket[#t] if the current article is checked into the Fossil repo, @racket[#f] otherwise. - -} - -@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, the hash of @racket[(string->path ".")] is used. 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] - -Find the value of @racket[_key] in the supplied list of attributes, returning the value of -@racket[_missing-expr] if it’s not there. - -I had to write this because @racket[attr-ref] wants a whole tagged X-expression (not just the -attributes); also, by default it raises an exception when @racket[_key] is missing, rather than -returning an empty string. - -@defproc[(maybe-meta [key symbolish?] [missing-expr any/c ""]) any/c] - -Look up a value in @code{(current-metas)} that may or may not be present, returning the value of -@racket[_missing-expr] if it’s not there. - -@defproc[(tx-strs [tx txexpr?]) string?] - -Finds all the strings from the @emph{elements} of @racket[_tx] (ignoring attributes) and -concatenates them together. - -@examples[#:eval dust-eval -(tx-strs '(p [[class "intro"]] - (em "I’m not opening the safe") ", Wilson remembers thinking."))] - -@defproc[(make-tag-predicate [sym symbol?] ...) (-> any/c boolean?)] - -Returns a function (or @italic{predicate}) that returns @racket[#t] if its argument is -a @racket[_txexpr] whose tag matches any @racket[_sym]. This predicate is useful for passing as the -@racket[_pred] expression in functions @racket[splitf-txexpr] and @racket[findf-txexpr]. - -@examples[#:eval dust-eval -(define is-aside? (make-tag-predicate 'aside 'sidebar)) - -(is-aside? '(q "I am not mad, Sir Topas. I say to you this house is dark.")) -(is-aside? '(aside "How smart a lash that speech doth give my Conscience?")) -(is-aside? '(sidebar "Many copies that we use today are conflated texts."))] - -@defproc[(first-words [txprs (listof txexpr?)] [n exact-nonnegative-integer?]) string?] - -Given a list of tagged X-expressions, returns a string containing the first @racket[_n] words found -in the string elements of @racket[_txprs], or all of the words if there are less than @racket[_n] -words available. Used by @racket[default-title]. - -This function aims to be smart about punctuation, and equally fast no matter how large the list of -elements that you send it. - -@examples[#:eval dust-eval -(define txs-decimals - '((p "Four score and 7.8 years ago — our fathers etc etc"))) -(define txs-punc-and-split-elems - '((p "“Stop!” she called.") (p "(She was never one to be silent.)"))) -(define txs-dashes - '((p [[class "newthought"]] (span [[class "smallcaps"]] "One - and") " only one.") - (p "That was all she would allow."))) -(define txs-parens-commas - '((p "She counted (" (em "one, two") "— silently, eyes unblinking"))) -(define txs-short - '((span "Not much here!"))) - -(first-words txs-decimals 5) -(first-words txs-punc-and-split-elems 5) -(first-words txs-dashes 5) -(first-words txs-parens-commas 5) -(first-words txs-short 5) -] - -@defproc[(normalize [str string?]) string?]{ - -Removes all non-space/non-alphanumeric characters from @racket[_str], converts it to lowercase, and -replaces all spaces with hyphens. - -@examples[#:eval dust-eval -(normalize "Why, Hello World!") -(normalize "My first-ever 99-argument function, haha") -] - -} - -@section{Article parsers and helpers} - -@defparam[listing-context ctxt (or/c 'blog 'feed 'print "") #:value ""] - -A parameter specifying the current context where any listings of articles would appear. Its purpose -is to allow articles to exclude themselves from certain special collections (e.g., the blog, the RSS -feed, print editions). Any article whose @code{conceal} meta matches the current context will not be -included in any listings returned by the listing functions in -@seclink["cache-rkt"]{@filepath{cache.rkt}}. - -@defproc[(default-title [body-txprs (listof txexpr?)]) string?] - -Given a list of tagged X-expressions (the elements of an article’s doc, e.g.), returns a string -containing a suitable title for the document. (Uses @racket[first-words].) - -Titles are not required for articles, but there are contexts where you need something that serves as -a title if one is not present, and that’s what this function supplies. - -@examples[#:eval dust-eval -(define doc - '(root (p "If I had been astonished at first catching a glimpse of so outlandish an " - "individual as Queequeg circulating among the polite society of a civilized " - "town, that astonishment soon departed upon taking my first daylight " - "stroll through the streets of New Bedford…"))) -(default-title (get-elements doc))] - -@defproc[(current-series-pagenode) pagenode?] - -If @code{(current-metas)} has the key @racket['series], converts its value to the pagenode pointing to -that series, otherwise returns @racket['||]. - -@examples[#:eval dust-eval -(require pollen/core) -(parameterize ([current-metas (hash 'series "marquee-fiction")]) - (current-series-pagenode))] - -@defproc[(current-series-noun) string?] - -If @code{(current-metas)} has the key @racket['series] and if there is a corresponding -@racket[series] in the @racket[series-list], return its @racket[series-noun-singular] value; -otherwise return @racket[""]. - -@defproc[(current-series-title) string?] - -If @code{(current-metas)} has the key @racket['series] and if there is a corresponding -@racket[series] in the @racket[series-list], return its @racket[series-title] value; -otherwise return @racket[""]. - -@defproc[(invalidate-series) (or/c void? boolean?)] - -If the current article specifies a @racket['series] meta, and if a corresponding @filepath{.poly.pm} -file exists in @racket[series-folder], attempts to “touch” the last-modified timestamp on that file, -returning @racket[#t] on success or @racket[#f] on failure. If either precondition is not true, -returns @|void-const|. - -When an article is being rendered, that means the article has changed, and if the article has -changed, its series page (if any) should be updated as well. Touching the @filepath{.poly.pm} file -for a series page triggers a re-render of that page when running @tt{make web} to rebuild the web -content (see @repo-file{makefile}). - -Only used in one place, @repo-file{tags-html.rkt}. - -@defproc[(disposition-values [str string?]) any] - -Given a string @racket[_str], returns two values: the portion of the string coming before the first -space, and the rest of the string. - -@examples[#:eval dust-eval -(disposition-values "* thoroughly recanted")] - -@defproc[(build-note-id [tx txexpr?]) non-empty-string?] - -Given a @code{note} tagged X-expression, returns an identifier string to uniquely identify that note -within an article. This identifier is used as an anchor link in the note’s HTML, and as part of the -note’s primary key in the SQLite cache database. - -@examples[#:eval dust-eval -(build-note-id '(note [[date "2018-02-19"]] "This is an example note")) -(build-note-id '(note [[date "2018-03-19"] [author "Dean"]] "Different author!")) -] - -@defproc[(notes->last-disposition-values [txprs (listof txexpr?)]) any] - -Given a list of tagged X-expressions (ideally a list of @code{note}s), returns two values: the value -of the @racket['disposition] attribute for the last note that contains one, and the ID of that note. - -@examples[#:eval dust-eval -(define notelist - (list - '(note [[date "2018-02-19"] [disposition "* problematic"]] "First note") - '(note [[date "2018-03-19"]] "Second note") - '(note [[date "2018-04-19"] [disposition "† recanted"]] "Third note"))) - -(notes->last-disposition-values notelist)] - -@section{Date formatters} - -@defproc[(ymd->english [ymd-string string?]) string?] - -Converts a date-string of the form @code{"YYYY-MM-DD"} to a string of the form @code{"Monthname D, -YYYY"}. - -If the day number is missing from @racket[_ymd-string], the first day of the month is assumed. If -the month number is also missing, January is asssumed. If the string cannot otherwise be parsed as -a date, an exception is raised. - -If any spaces are present in @racket[_ymd-string], everything after the first space is ignored. - -@defproc[(ymd->dateformat [ymd_string string?] [dateformat string?]) string?] - -Converts a date-string of the form @code{"YYYY-MM-DD"} to another string with the same date -formatted according to @racket[_dateformat]. The -@ext-link["http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table"]{pattern syntax -of the date format} comes from the Unicode CLDR. DELETED code-docs/main.scrbl Index: code-docs/main.scrbl ================================================================== --- code-docs/main.scrbl +++ code-docs/main.scrbl @@ -1,58 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt" - racket/runtime-path - (for-label racket/base - "../crystalize.rkt")) - -@title{Local Yarn: Source Code Notes} - -@author{Joel Dueck} - -These are my notes about the internals of the Local Yarn source code. In other words, a personal -reference, rather than a tutorial. - -You’ll get the most out of these notes if you have read @other-doc['(lib -"pollen/scribblings/pollen.scrbl")], and worked through the tutorials there by hand. - -@margin-note{Note that these pages are heavily interlinked with the central Racket documentation at -@tt{docs.racket-lang.org}, which are written and maintained by others. - -Some links from those pages will not work unless you @ext-link["#"]{open this page in its own tab}. -} - -Here’s a rough diagram showing how the @tt{.rkt} modules in this project relate to each other, and -to the Pollen source documents. This is the least complex system I could devise that would @tt{A)} -implement everything I want in my @secref["design-goals"], @tt{B)} cleanly separate dependencies for -print and web output, and @tt{C)} organize an ever-growing collection of hundreds of individual -notes and articles without noticable loss of speed. - -@(define-runtime-path source-diagram "source-diagram.png") -@centered{@responsive-retina-image[source-diagram]} - -The modules are arranged vertically: those on the upper rows provide bindings which are used by -those on the lower rows. The bottom row are the @tt{.poly.pm} files that make up @tech{articles} and -@tech{series}. - -Individual articles, while they are being rendered to HTML pages, save copies of their metadata and -HTML to the SQLite cache. This is done by calling @racket[parse-and-cache-article!] from within -their template. - -Any pages that gather content from multiple articles, such as Series pages and the RSS feed, pull -this content directly from the SQLite cache. This is much faster than trawling through Pollen’s -cached metas of every article looking for matching articles. - -@local-table-of-contents[] - -@include-section["tour.scrbl"] -@include-section["design.scrbl"] -@include-section["pollen.scrbl"] @; pollen.rkt -@include-section["series.scrbl"] -@include-section["dust.scrbl"] @; dust.rkt -@include-section["snippets-html.scrbl"] @; you get the idea -@include-section["cache.scrbl"] -@include-section["crystalize.scrbl"] -@include-section["other-files.scrbl"] DELETED code-docs/other-files.scrbl Index: code-docs/other-files.scrbl ================================================================== --- code-docs/other-files.scrbl +++ code-docs/other-files.scrbl @@ -1,42 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt") - -@(require (for-label racket/base "../cache.rkt")) - -@title[#:tag "other-files"]{Other files} - -@section{Home page (@filepath{index.html.pp})} - -Simple Pollen preprocessor file that generates the home page. - -@section{Keyword Index (@filepath{keyword-index.rkt})} - -Through its provided @tt{main} function, builds the keyword index page by pulling all the index -entries directly from the SQLite cache and sorting them by first letter. - -@section{Blog (@filepath{blog.rkt})} - -Through its provided @tt{main} function, creates a paginated listing of all @tech{articles} and -@tech{notes}. - -@section{RSS Feed (@filepath{rss-feed.rkt})} - -Through its provided @tt{main} function, creates the RSS feed in the file @filepath{feed.xml}. Both -articles and notes are included. Any article or note with either @racket["all"] or @racket["feed"] -in its @racket['conceal] meta is excluded. - -@section{Cache initialization (@filepath{util/init.rkt})} - -Creates and initializes the cache database with @racket[init-cache-db!] and -@racket[preheat-series!]. - -@section{New article template (@filepath{util/newpost.rkt})} - -Prompts for a title, creates an article with a normalized version of the filename and today’s date, -and opens the article in an editor. - - DELETED code-docs/pollen.scrbl Index: code-docs/pollen.scrbl ================================================================== --- code-docs/pollen.scrbl +++ code-docs/pollen.scrbl @@ -1,382 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt") -@(require (for-label "../pollen.rkt" - "../dust.rkt" - "../cache.rkt" - "../crystalize.rkt" - racket/base - racket/contract - racket/string - txexpr - pollen/tag - pollen/setup - pollen/core - sugar/coerce)) - -@title[#:tag "pollen-rkt"]{Pollen} - -@defmodule["pollen.rkt" #:packages ()] - -The file @filepath{pollen.rkt} is implicitly @code{require}d in every template and every @code{#lang -pollen} file in the project. It defines the markup for all Pollen documents, and also re-provides -everything provided by @seclink["cache-rkt"]{@filepath{cache.rkt}} and -@seclink["crystalize-rkt"]{@filepath{crystalize.rkt}}. - -The @code{setup} module towards the top of the file is used as described in -@racketmodname[pollen/setup]. - -@section{Markup reference} - -These are the tags that can be used in any of @italic{The Local Yarn}’s Pollen documents (articles, -etc). - -@defproc[(title [element xexpr?] ...) txexpr?]{ - -Supplies a title for the document. You can use any otherwise-valid markup within the title tag. - -Titles are optional; if you don’t specify a title, the article will appear without one. This is -a feature! -} - -@deftogether[(@defproc[(excerpt [elements xexpr?] ...) txexpr?] - @defproc[(excerpt* [elements xexpr?] ...) txexpr?])]{ - -Specify an excerpt to be used when the article or note included in an excerpt-style listing (such as -the blog). The contents of @racket[excerpt] will be extracted out of the article and note and only -appear in listings; if @racket[excerpt*] is used, its contents will be left in place in the -article/note and @emph{reused} as the excerpt in listings. - -} - -@defproc[(p [element xexpr?] ...) txexpr?]{ - -Wrap text in a paragraph. You almost never need to use this tag explicitly; -just separate paragraphs by an empty line. - -Single newlines within a paragraph will be replaced by spaces, allowing you to use -@ext-link["https://scott.mn/2014/02/21/semantic_linewrapping/"]{semantic line wrapping}. -} - -@defproc[(newthought [element xexpr?] ...) txexpr?]{ - -An inline style intended for the first few words of the first paragraph in a new section. Applies -a “small caps” style to the text. Any paragraph containing a @code{newthought} tag is given extra -vertical leading. - -Rule of thumb: within an article, use either @code{section}/@code{subsection} or @code{newthought} -to separate sections of text, but not both. Even better, keep it consistent across articles within -a series. - -If you just need small caps without affecting the paragraph, use @racket[caps]. -} - -@deftogether[(@defproc[(section [element xexpr?] ...) txexpr?] - @defproc[(subsection [element xexpr?] ...) txexpr?])]{ - -Create second- and third-level headings, respectively. This is counting the article's title as the -first-level header (even if the current article has no title). - -} - -@defproc[(block [element xexpr?] ...) txexpr?]{ - -A container for content that should appear grouped together on larger displays. Intended for use in -Series pages, where the template is very minimal to allow for more customization. You would want -output from @racket[(fenced-listing (articles 'short))] to appear inside a @racket[block], but when -using @racket['excerpt] or @racket['full] in place of @racket['short] in that code, you would want -the output to appear outside it (since the “full” and “excerpt” versions of each article effectively -supply their own blocks). Only relevant to HTML output. - -} - -@deftogether[(@defproc[(link [link-id stringish?] [link-text xexpr?]) txexpr?] - @defproc[(url [link-id stringish?] [url string?]) void?])]{ - -All hyperlinks are specified reference-style. So, to link some text, use the @code{link} tag with -an identifier, which can be a string, symbol or number. Elsewhere in the text, use @code{url} with -the same identifier to specify the URL: - -@codeblock|{ - #lang pollen - If you need help, ◊link[1]{Google it}. - - ◊url[1]{https://google.com} -}| - -The @code{url} tag for a given identifier may be placed anywhere in the document, even before it is -referenced. If you create a @code{link} for an identifier that has no corresponding @code{url}, -a @code{"Missing reference: [link-id]"} message will be substituted for the URL. Conversely, -creating a @code{url} that is never referenced will produce no output and no warnings or errors. - -} - -@defproc*[([(xref [title string?]) txexpr?] - [(xref [article-base string?] [element xexpr?] ...) txexpr?])]{ - -Hyperlink to another article within @italic{The Local Yarn} using an @racket[_article-base], which -is the base filename only of an @tech{article} within @racket[articles-folder] (without the -@filepath{.poly.pm} extension). - -If a single argument is supplied (@racket[_title]) it is typeset italicized as the link text, and -its @racket[normalize]d form is used as the article base to generate the link. If more than one -argument is supplied, the first is used as the article base, and the rest are used as the contents -of the link. - -@codeblock|{ - #lang pollen - - A link to ◊xref{My Ultimate Article} will link to “my-ultimate-article.poly.pm”. - A link using ◊xref["my-ultimate-article"]{this form} goes to the same place. -}| - -} - -@deftogether[(@defproc[(figure [image-file string?] [caption xexpr?] ...) txexpr?] - @defproc[(figure-@2x [image-file string?] [caption xexpr?] ...) txexpr?])]{ - -Insert a block-level image. The @racket[_image-file] should be supplied as a filename only, with no -folder names. It is assumed that the image is located inside an @racket[images-folder] within the -same folder as the source document. - -For web output, using @racket[figure-@2x] will produce an image hard-coded to display at half its -actual size, or the width of the text block, whichever is smaller. - -} - -@defproc[(image-link [image-file string?] [link-text xexpr?] ...) txexpr?]{ - -Adds a hyperlink to @racket[_image-file], supplied as a filename only with no folder names. It is -assumed that the image is located inside an @racket[images-folder] within the same folder as the -source document. - -} - -@deftogether[(@defproc[(fn [fn-id stringish?]) txexpr?] - @defproc[(fndef [fn-id stringish?] [elements xexpr?] ...) txexpr?])]{ - -As with hyperlinks, footnotes are specified reference-style. In the output, footnotes will be -numbered according to the order in which their identifiers are referenced in the source document. - -Example: - -@codeblock|{ - #lang pollen - Shoeless Joe Jackson was one of the best players of all time◊fn[1]. - - ◊fndef[1]{But he might have lost the 1919 World Series on purpose.} -}| - -You can refer to a given footnote definition more than once. - -The @code{fndef} for a given id may be placed anywhere in the source document, even before it is -referenced. If you create a @code{fn} reference without a corresponding @code{fndef}, -a @code{"Missing footnote definition!"} message will be substituted for the footnote text. -Conversely, creating a @code{fndef} that is never referenced will produce no output, warning or -error. - -} - -@deftogether[(@defproc[(dialogue [elements xexpr?] ...) txexpr?] - @defproc[(say [interlocutor string?] [elements xexpr?] ...) txexpr?] - @defproc[(saylines [interlocutor string?] [elements xexpr?] ...) txexpr?])]{ - -Use these tags together for transcripts of dialogue, chats, screenplays, interviews and so -forth. The @racket[saylines] tag is the same as @racket[say] except that within @racket[saylines], -linebreaks within paragraphs are preserved. - -Example usage: - -@codeblock|{ - #lang pollen - - ◊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 [#:key key string? ""] [elements xexpr?] ...) txexpr?]{ - -Creates a bidirectional link between this spot in the document and an entry in the keyword index -under @racket[_key]. If @racket[_key] is not supplied, the string contents of @racket[_elements] are -used as the key. Use @tt{!} to split @racket[_key] into a main entry and a subentry. - -The example below will create two index entries, one under the heading “compassion” and one under -the main heading "cats" and a subheading “stray”: - -@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}. That if we lavish our concern on every - stray ◊index[#:key "cats!stray"]{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?]{ - -Add a @tech{note} to the “Further Notes” section of the article. - -The @code{#:date} attribute is required and must be of the form @tt{YYYY-MM-DD}. - -The @code{#:author} and @code{#:author-url} attributes can be used to credit notes from other -people. If the @code{#:author} attribute is not supplied then the value of @code{default-authorname} -is used. - -The @code{#:disposition} attribute is used for notes that update or alter the whole disposition of -the article. It must be a string of the form @racket[_mark _past-tense-verb], where @racket[_mark] -is a symbol suitable for use as a marker, such as * or †, and @racket[_past-tense-verb] is the word -you want used to describe the article’s current state. An article stating a metaphysical position -might later be marked “recanted”; a prophecy or prediction might be marked “fulfilled”. - -@codeblock|{ -#lang pollen - -◊note[#:date "2019-02-19" #:disposition "✓ verified"]{I wasn’t sure, but now I am.} -}| - - -If more than one note contains a @code{disposition} attribute, the one from the most recent note is -the one used. - -Some caveats (for now): - -@itemlist[ - @item{Avoid defining new footnotes using @code{fndef} inside a @code{note}; these footnotes will - be placed into the main footnote section of the article, which is probably not what you want.} -] - -} - -@defproc[(verse [#:title title string? ""] [#:italic? italic boolean? #f] [element xexpr?] ...) - txexpr?]{ - -Typeset contents as poetry, with line breaks preserved and the block centered on the longest line. -To set the whole block in italic, use @code{#:italic? #t} — otherwise, use @code{i} within the -block. - -If the first element in an article is a @racket[verse] tag with the @racket[#:title] attribute -specified, that title is used as the article’s title if the normal @racket[title] tag is absent. - -To cite a source, use @racket[attrib] immediately afterward. - -} - -@defproc[(magick [element xexpr?] ...) txexpr?]{ - -Typeset contents using historical ligatures and the “long s” conventions of 17th-century English -books. - -} - -@defproc[(blockquote [element xexpr?] ...) txexpr?]{ - -Surrounds a block quotation. To cite a source, use @racket[attrib] immediately afterward. - -} - -@defproc[(attrib [element xexpr?] ...) txexpr?]{ - -An attribution line, for citing a source for a block quotation, epigraph or poem. - -} - -@defproc[(blockcode [element xexpr?] ...) txexpr?]{ - -Typeset contents as a block of code using a monospace font. Line breaks are preserved. - -} - -@deftogether[(@defproc[(i [element xexpr?] ...) txexpr?] - @defproc[(em [element xexpr?] ...) txexpr?] - @defproc[(b [element xexpr?] ...) txexpr?] - @defproc[(mono [element xexpr?] ...) txexpr?] - @defproc[(strong [element xexpr?] ...) txexpr?] - @defproc[(strike [element xexpr?] ...) txexpr?] - @defproc[(ol [element xexpr?] ...) txexpr?] - @defproc[(ul [element xexpr?] ...) txexpr?] - @defproc[(item [element xexpr?] ...) txexpr?] - @defproc[(sup [element xexpr?] ...) txexpr?] - @defproc[(caps [element xexpr?] ...) txexpr?] - @defproc[(code [element xexpr?] ...) txexpr?])]{ - -Work pretty much how you’d expect. - -} - -@section{Convenience macros} - -@defform[(for/s thing-id listofthings result-exprs ...) - #:contracts ([listofthings (listof any/c)])]{ - -A shorthand form for Pollen’s @code{for/splice} that uses far fewer brackets when you’re only -iterating through a single list. - -@codeblock|{ -#lang pollen - -◊for/s[x '(7 8 9)]{Now once for number ◊x} - -◊;Above line is shorthand for this one: -◊for/splice[[(x (in-list '(7 8 9)))]]{Now once for number ◊x} -}| - -} - -@section{Defining new tags} - -I use a couple of macros to define tag functions that automatically branch into other functions -depending on the current output target format. This allows me to put the format-specific tag -functions in separate files that have separate places in the dependency chain. So if only the HTML -tag functions have changed and not those for PDF, the @filepath{makefile} can ensure only the HTML -files are rebuilt. - -@defproc[#:kind "syntax" - (poly-branch-tag (tag-id symbol?)) - (-> txexpr?)]{ - -Defines a new function @racket[_tag-id] which will automatically pass all of its arguments to a -function whose name is the value returned by @racket[current-poly-target], followed by a hyphen, -followed by @racket[_tag]. So whenever the current output format is @racket['html], the function -defined by @racket[(poly-branch-tag _p)] will branch to a function named @racket[html-p]; when the -current format is @racket['pdf], it will branch to @racket[pdf-p], and so forth. - -You @emph{must} define these branch functions separately, and you must define one for @emph{every} -output format included in the definition of @racket[poly-targets] in this file’s @racket[setup] -submodule. If you do not, you will get “unbound identifier” errors at expansion time. - -The convention in this project is to define and provide these branch functions in separate files: -see, e.g., @filepath{tags-html.rkt}. - -Functions defined with this macro @emph{do not} accept keyword arguments. If you need keyword -arguments, see @racket[poly-branch-kwargs-tag]. - -@margin-note{The thought behind having two macros so similar is that, by cutting out handling for keyword -arguments, @racket[poly-branch-tag] could produce simpler and faster code. I have not verified if -this intuition is meaningful or correct.} - -} - -@defproc[#:kind "syntax" - (poly-branch-kwargs-tag (tag-id symbol?)) - (-> txexpr?)]{ - -Works just like @racket[poly-branch-tag], but uses Pollen’s @racket[define-tag-function] so that -keyword arguments will automatically be parsed as X-expression attributes. - -Additionally, the branch functions called from the new function must accept exactly two arguments: -a list of attributes and a list of elements. - -} DELETED code-docs/scribble-helpers.rkt Index: code-docs/scribble-helpers.rkt ================================================================== --- code-docs/scribble-helpers.rkt +++ code-docs/scribble-helpers.rkt @@ -1,79 +0,0 @@ -#lang racket/base - -; SPDX-License-Identifier: BlueOak-1.0.0 -; This file is licensed under the Blue Oak Model License 1.0.0. - -;; Convenience/helper functions for this project’s Scribble documentation - -(require scribble/core - scribble/manual/lang - scribble/html-properties - scribble/private/manual-sprop - scribble/decode - racket/runtime-path - (only-in net/uri-codec uri-encode)) -(provide (all-defined-out)) - -(define-runtime-path custom-css "custom.css") - -(define repo-url/ "https://thelocalyarn.com/code/") - -;; Link to a ticket on the Fossil repository by specifying the ticket ID. -;; The "_parent" target breaks out of the iframe used by the Fossil repo web UI. -(define (ticket id-str) - (hyperlink (string-append repo-url/ "tktview?name=" id-str) - "ticket " - (tt id-str) - #:style (style #f (list (attributes '((target . "_parent"))))))) - -;; Link to a wiki page on the Fossil repository by specifying the title -(define (wiki title) - (hyperlink (string-append repo-url/ "wiki?name=" (uri-encode title)) - title - #:style (style #f (list (attributes '((target . "_parent"))))))) - -;; Link somewhere outside these docs or Racket docs. The `_blank` target opens in a new tab. -(define (ext-link url-str . elems) - (keyword-apply hyperlink '(#:style) (list (style #f (list (attributes '((target . "_blank")))))) - url-str - elems)) - -;; Link to show contents of the latest checked-in version of a file -;; (or a file listing if a directory was specified) -(define (repo-file filename) - (hyperlink (string-append repo-url/ "file/" filename) - (tt filename) - #:style (style #f (list (attributes '((target . "_parent"))))))) - -(define (responsive-retina-image img-path) - (image img-path - #:scale 0.5 - #:style (style #f (list (attributes '((style . "max-width:100%;height:auto;"))))))) - -;; -;; From https://github.com/mbutterick/pollen/blob/master/pollen/scribblings/mb-tools.rkt -;; - -(define (terminal . args) - (compound-paragraph (style "terminal" (list (css-style-addition custom-css) (alt-tag "div"))) - (list (apply verbatim args)))) - -(define (cmd . args) - (elem #:style (style #f (list (color-property "black"))) (tt args))) - -(define (fileblock filename . inside) - (compound-paragraph - (style "fileblock" (list* (alt-tag "div") 'multicommand - (box-mode "RfileboxBoxT" "RfileboxBoxC" "RfileboxBoxB") - scheme-properties)) - (list - (paragraph (style "fileblock_filetitle" (list* (alt-tag "div") (box-mode* "RfiletitleBox") scheme-properties)) - (list (make-element - (style "fileblock_filename" (list (css-style-addition custom-css))) - (if (string? filename) - (filepath filename) - filename)))) - (compound-paragraph - (style "fileblock_filecontent" (list* (alt-tag "div") (box-mode* "RfilecontentBox") scheme-properties)) - (decode-flow inside))))) - DELETED code-docs/scribble-iframe.html Index: code-docs/scribble-iframe.html ================================================================== --- code-docs/scribble-iframe.html +++ code-docs/scribble-iframe.html @@ -1,13 +0,0 @@ -<div class='fossil-doc' data-title='Code Documentation' > -<!-- SPDX-License-Identifier: BlueOak-1.0.0 - This file is licensed under the Blue Oak Model License 1.0.0. ---> - <div class='iframe-surround'> - <iframe id='scribble' src="index.html" class="embedded-docs"> - </iframe> - </div> -</div> - - <script> - document.getElementById('scribble').src = "index.html?n=" + new Date()/1; - </script> DELETED code-docs/series.scrbl Index: code-docs/series.scrbl ================================================================== --- code-docs/series.scrbl +++ code-docs/series.scrbl @@ -1,74 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt" - scribble/example) - -@(require (for-label "../pollen.rkt" - "../series-list.rkt" - "../dust.rkt" - "../cache.rkt" - pollen/core - racket/base - racket/contract)) - -@title{Defining Series} - -To create a new series: - -@itemlist[#:style 'ordered - - @item{Create a file @filepath{my-key.poly.pm} inside @racket[series-folder] and include a call - to @racket[fenced-listing] to list all the articles and notes that will be included in the series: - @codeblock|{ -#lang pollen - -◊(define-meta title "My New Series") - -◊block{Here’s what we call a bunch of similar articles: - -◊(fenced-listing (articles 'short)) - -} - }| - } - - @item{Add an entry for @racket[_my-key] to @racket[series-list] in @filepath{series-list.rkt}} - - @item{Use @racket[(define-meta series "my-key")] in articles to add them to the series.} - - @item{If @racket[series-ptree-ordered?] is @racket[#t], create a @seclink["Pagetree" #:doc '(lib - "pollen/scribblings/pollen.scrbl")]{pagetree} file in @racket[series-folder] named - @filepath{my-key.ptree}.} - - ] - -@section{Series list} - -@defmodule["series-list.rkt" #:packages ()] - -This module contains the most commonly used bits of meta-info about @tech{series}. Storing these -bits in a hash table of structs makes them faster to retrieve than when they are stored inside the -metas of the Pollen documents for the series themselves. - -@defthing[series-list hash?]{ - -An immutable hash containing all the title and noun info for each @tech{series}. Each key is -a string and each value is a @racket[series] struct. - -} - -@defstruct[series ([key string?] - [title string?] - [noun-plural string?] - [noun-singular string?] - [ptree-ordered? boolean?])]{ - -Struct for holding metadata for a @tech{series}. The @racket[_ptree-ordered?] value should be -@racket[#t] if there is a @filepath{@italic{key}.ptree} file in @racket[series-folder] that provides -information on how articles in the series are ordered. - -} - DELETED code-docs/snippets-html.scrbl Index: code-docs/snippets-html.scrbl ================================================================== --- code-docs/snippets-html.scrbl +++ code-docs/snippets-html.scrbl @@ -1,161 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt") - -@(require (for-label "../pollen.rkt" - "../dust.rkt" - "../snippets-html.rkt" - racket/base - racket/contract - racket/string - pollen/template - pollen/pagetree - txexpr - sugar/coerce)) - -@title{HTML snippets} - -@defmodule["snippets-html.rkt" #:packages ()] - -Each “snippet” module provides all (well @emph{most of}) the document- and article-level blocks of -structural markup necessary for a particular target output format; this one is for HTML. The idea is -that any block of markup that might be reused across more than one template should be a function. - -The functions in the snippets modules follow two conventions in this project: - -@itemlist[ - @item{Functions that return strings of HTML have the prefix @tt{html$-}.} - @item{Such functions do not do any parsing or destructuring of complex objects; every separate - piece that will be inserted into the template is passed in as a separate argument. This makes it - harder to change the scope of what a snippet does, but makes things faster since all the parsing - can happen in one place, before the snippet functions are called.} ] - -@section{Using @tt{pollen/mode}} - -It’s worth looking at the source for this file to see how @racketmodname[pollen/mode] can be used to -make it easy to write “mini-template” functions: - -@codeblock{ -#lang pollen/mode racket/base - -(define (html$-my-article title body-tx) - ◊string-append{ - <p><b>◊|title|</b><p> - ◊(->html body-tx) - }) -} - -@section{HTML Snippet functions} - -@defproc[(html$-page-head [title (or/c string? #f) #f] [close-head? boolean? #t]) -non-empty-string?]{ - -Returns the @tt{<head>} section of an HTML document. - -If @racket[_title] is a string it will be used inside the @tt{<title>} tag. - -If you want to include additional stuff inside the @tt{<head>}, you can set @racket[_close-head?] to -@racket[#f] to prevent it from including the closing @tt{</head>} tag (you’ll have to add it -yourself). -} - -@defproc[(html$-page-body-open [body-class string? ""]) non-empty-string?]{ - -Returns the opening @tt{<body>} and @tt{<main>} tags and elements that immediately follow, such as -site header, logo and navigation. - -If @racket[_body-class] is a non-empty string, its contents will be included in the @tt{class} -attribute of the @tt{<body>} tag. -} - -@defproc[(html$-series-list) non-empty-string?]{ - -Returns an HTML @tt{<section>} containing a list of all series, grouped by their “plural nouns”. The -grouped list will flow into columns on wider displays. -} - -@defproc[(html$-article-open [pagenode pagenode?] - [title-specified-in-doc? boolean?] - [title txexpr?] - [pubdate string?]) - non-empty-string?]{ - -Returns the opening @tt{<article>} tag and elements that immediately follow: permlink, publish date, -and opening @tt{<section>} tag. - -The @racket[_title-specified-in-doc?] form changes the HTML markup structure used. -} - -@defproc[(html$-article-close [footertext string?]) non-empty-string?]{ - -Returns a string containing a closing @tt{<section>} tag, a @tt{<footer>} element containing -@racket[_footertext], and a closing @tt{<article>} tag. If @racket[_footertext] is empty, the -@tt{<footer>} element will be omitted. -} - -@defproc[(html$-article-listing-short [pagenode pagenode?] [pubdate string?] [title string?]) -non-empty-string?]{ - -Returns a string of HTML for an article as it would appear in a listing context in “short” form -(date and title only, with link). -} - -@defproc[(html$-page-footer) non-empty-string?]{ -Returns an HTML @tt{<footer>} tag for use at the bottom of each page. -} - -@defproc[(html$-page-body-close) non-empty-string?]{ - -Returns a string containing the page’s @tt{<footer>} and closing tags. -} - -@defproc[(html$-note-contents [disposition-mark string?] [elems (listof xexpr?)]) -non-empty-string?]{ - -Returns a string containing the body-elements of a note converted to HTML. If -@racket[_disposition-mark] is not empty, a @tt{<span>} containing it will be inserted as the first -element of the first block-level element. -} - -@defproc[(html$-note-listing-full [pagenode pagenode?] - [note-id string?] - [title-html-flow string?] - [date string?] - [contents string?] - [author string? (default-authorname)] - [author-url string? ""]) - non-empty-string?]{ - -Returns a string containing the complete HTML markup for a @racket[note], including title, permlink, -date, contents and author, suitable for display outside the context of its parent article. -} - -@defproc[(html$-note-in-article [id string?] - [date string?] - [contents string?] - [author string?] - [author-url string?]) non-empty-string?]{ - -Like @racket[html$-note-listing-full], but returns HTML for a @racket[note] suitable for display -inside its parent article. -} - -@defproc[(html$-notes-section [note-htmls string?]) non-empty-string?]{ - -Returns the complete HTML for the @italic{Further Notes} section of an article. -} - -@defproc[(html$-paginate-navlinks [current-page exact-positive-integer?] - [pagecount exact-positive-integer?] - [basename string?]) string?]{ - -On the “blog”, the articles are split across multiple files: @filepath{blog-pg1.html}, -@filepath{blog-pg2.html}, etc. This function provides a string containing HTML for a group of links -that can be given within each file, to link to the pages that come before/after it. - -The links are enclosed within @tt{<li>} tags. It’s up to the calling site to provide the enclosing -@tt{<ul>} tag (in case any custom styling or IDs need to be applied). -} DELETED code-docs/source-diagram.png Index: code-docs/source-diagram.png ================================================================== --- code-docs/source-diagram.png +++ code-docs/source-diagram.png cannot compute difference between binary files DELETED code-docs/tour.scrbl Index: code-docs/tour.scrbl ================================================================== --- code-docs/tour.scrbl +++ code-docs/tour.scrbl @@ -1,137 +0,0 @@ -#lang scribble/manual - -@; SPDX-License-Identifier: BlueOak-1.0.0 -@; This file is licensed under the Blue Oak Model License 1.0.0. - -@(require "scribble-helpers.rkt") - -@(require (for-label racket/base pollen/core "../pollen.rkt")) - -@title{How I Publish: A Quick Tour} - -This isn’t a tutorial, since these steps probably won’t all work on your computer. Think of these -narrations like me talking while I drive. - -@section{Creating an article} - -Open a terminal window. - -@terminal{@cmd{> cd /path/to/thelocalyarn}} - -The @tt{make} command provides a high-level control panel for common tasks. Typing just make from -a terminal window shows a list of options: - -@terminal{ -@cmd{> make} -article Start a new article from a template -help Displays this help screen -publish Sync all HTML and PDF stuff to the public web server -scribble Rebuild code documentation and update Fossil repo -web Rebuild all web content (not PDFs) -zap Clear Pollen and Scribble cache, and remove all HTML output -} - -Following the first option in this list, I type @tt{make article}, and enter a post title when -prompted: - -@terminal{ -@cmd{> make article} -racket -tm util/newpost.rkt -Enter title: @cmd{My New Post} -} - -The script creates a new @filepath{.poly.pm} file using a normalized version of the title for the -filename, and opens it in my editor (this is currently hardcoded to use MacVim). When the file pops -up we see a basic template ready to edit: - -@filebox["articles/my-new-post.poly.pm" -@codeblock|{ -#lang pollen - -◊; Copyright 2020 by Joel Dueck. All Rights Reserved. -◊(define-meta conceal "blog,rss") -◊(define-meta published "2020-01-18") - -◊title{My New Post} - -Write here! -}|] - -At this point I might delete the @tt{◊title} line, since specifying a formal title is optional -(other than the one needed to generate the filename). I might also add a @racket[define-meta] for -@tt{series} or @tt{topics}. - -As long as the @racket[define-meta] for @tt{conceal} contains @racket{rss}, the new article will not -appear in the RSS feed; as long as it contains @racket{blog} it will not appear in the blog. This is -useful for when an article is in a draft state, or simply when you want to keep it semi-hidden. - -When satisfied with the post I’ll remove the @racket[define-meta] for @tt{conceal}, save it one last -time, then go back to the terminal: - -@terminal{ -@cmd{> make web} -[lots of output: rebuilds blog pages, keyword index, RSS feed] -@cmd{> make publish} -[lots of output: uploads all web content to the server] -} - -The article also needs to be added to the Fossil repository, so revisions to it will be tracked: - -@terminal|{ -|@cmd{> fossil add article/my-new-post.poly.pm} -ADDED article/my-new-post.poly.pm - -|@cmd{> fossil commit -m "Publish ‘My New Post’"} -Autosync: https://joel@thelocalyarn.com/code -Round-trips: 2 Artifacts sent: 0 received: 1 -Pull done, sent: 892 received: 8720 ip: 162.243.186.132 -New_Version: 15507b62416716a3f0be3c444b0fc09aa4364b989140c5788cf679eb0b2463a6 -Autosync: https://joel@thelocalyarn.com/code -Round-trips: 2 Artifacts sent: 2 received: 1 -Sync done, sent: 10153 received: 4680 ip: 162.243.186.132 -}| - -As you can see, Fossil does an automatic pull before the commit, and another automatic push -afterwards. This commit is now visible on the public timeline, and the source code for the article -can now be seen on the public repo at @tt{thelocalyarn.com/code/}. - -@section{Adding notes to an article} - -A few days (or years) after doing the above, I receive an email from Marjorie with commenting on -@italic{My New Post} and I decide to publish her comments. - -I open the article in my editor and add some lines to the end: - -@filebox["articles/my-new-post.poly.pm" -@codeblock|{ -#lang pollen - -◊; Copyright 2020 by Joel Dueck. All Rights Reserved. -◊(define-meta published "2020-01-18") - -◊title{My New Post} - -It’s a 4⨉4 treated. I got twenty, actually, for the backyard fence. - -◊note[#:date "2020-01-23" #:author "Marjorie"]{ - Hope you sank that thing below the frost line. -} -}|] - -This looks like a blog-style comment, but the @racket[note] tag function has some special powers -that typical comments don’t have, as we’ll see in a moment. - -I save this, go back to the terminal and do @tt{make web} and @tt{make publish} as before. - -Now if you open the article’s permlink, you’ll see the note appears in a “Further Notes” section at -the bottom — again, just like a normal blog post comment. - -But if you go to the Blog section, you’ll see the note appearing in its own space right alongside -the other articles, as if it were a separate post. It will also appear in a separate entry in the -RSS feed. - -@section{What’s not here yet} - -Eventually there will be facilities for creating PDF files of individual articles, and print-ready -PDFs of books containing collections of articles. - DELETED crystalize.rkt Index: crystalize.rkt ================================================================== --- crystalize.rkt +++ crystalize.rkt @@ -1,285 +0,0 @@ -#lang racket/base - -; SPDX-License-Identifier: BlueOak-1.0.0 -; This file is licensed under the Blue Oak Model License 1.0.0. - -(require deta - db/base - threading - racket/match - racket/string - txexpr - pollen/template - pollen/decode - (except-in pollen/core select) ; avoid conflict with deta - ) - -(require "dust.rkt" "cache.rkt" "snippets-html.rkt") - -(provide parse-and-cache-article! - cache-index-entries-only!) - -(define current-title (make-parameter #f)) -(define current-excerpt (make-parameter #f)) -(define current-notes (make-parameter '())) -(define current-disposition (make-parameter "")) -(define current-disp-id (make-parameter "")) - -(define (filter-special-tags tx) - (match (get-tag tx) - ['title (current-title tx) ""] - ['excerpt (current-excerpt tx) ""] - ['excerpt* (current-excerpt tx) `(@ ,@(get-elements tx))] ; splice contents back in - ['note - (define note-id (build-note-id tx)) - (cond [(attrs-have-key? tx 'disposition) - (current-disp-id note-id) - (current-disposition (attr-ref tx 'disposition))]) - (current-notes (cons (attr-set tx 'note-id note-id) (current-notes))) ""] - [_ tx])) - -;; Save an article and its notes (if any) to the database, and return -;; (values plain-title [rendered HTML of the complete article]) -(define (parse-and-cache-article! pagenode doc) - (define body-txpr (decode doc #:txexpr-proc filter-special-tags)) - (current-notes (reverse (current-notes))) - (let* ([pubdate (select-from-metas 'published (current-metas))] - [doc-html (->html body-txpr #:splice? #t)] - [title-specified? (if (current-title) #t #f)] - [title-val (or (current-title) (check-for-poem-title doc))] - [title-tx (make-article-title pagenode - title-val - body-txpr - (current-disposition) - (current-disp-id))] - [title-html (->html title-tx #:splice? #t)] - [title-plain (tx-strs title-tx)] - [header (html$-article-open pagenode title-specified? title-tx pubdate)] - [series-node (current-series-pagenode)] - [footertext (make-article-footertext pagenode - series-node - (current-disposition) - (current-disp-id) - (length (current-notes)))] - [footer (html$-article-close footertext)] - [listing-short (html$-article-listing-short pagenode pubdate title-html)] - [listing-full (string-append header doc-html footer)] - [listing-excerpt (match (current-excerpt) - [#f listing-full] - [(var e) (string-append header (html$-article-excerpt pagenode e) footer)])] - [notes (extract-notes pagenode title-plain (current-notes))] - [notes-section-html (html$-notes-section (map cadr notes))]) - (thread - (lambda () - (call-with-transaction - (cache-conn) - (lambda () - (cache-index-entries! pagenode doc) ; note original doc is used here - (query-exec (cache-conn) - (delete (~> (from cache:note #:as n) - (where (= n.page ,(symbol->string pagenode)))))) - (apply insert! (cache-conn) (map car notes)) - (delete-article! pagenode) - (insert-one! (cache-conn) - (make-cache:article - #:page pagenode - #:title-plain title-plain - #:title-html-flow title-html - #:title-specified? title-specified? - #:published pubdate - #:updated (maybe-meta 'updated) - #:author (maybe-meta 'author default-authorname) - #:conceal (maybe-meta 'conceal) - #:series-page series-node - #:noun-singular (maybe-meta 'noun (current-series-noun)) - #:note-count (length (current-notes)) - #:content-html doc-html - #:disposition (current-disposition) - #:disp-html-anchor (current-disp-id) - #:listing-full-html listing-full - #:listing-excerpt-html listing-excerpt - #:listing-short-html listing-short)))))) - (values title-plain (string-append header doc-html notes-section-html footer)))) - -(define (check-for-poem-title doc-txpr) - (match (car (get-elements doc-txpr)) - [(txexpr 'div - (list (list 'class "poem")) - (list* (txexpr 'p - (list (list 'class "verse-heading")) - heading-elems) - _)) - `(title (span [[class "smallcaps"]] "‘" ,@heading-elems "’"))] - [_ '()])) - -;; Return a title txexpr for the current article, constructing a default if no title text was specified. -(define (make-article-title pagenode supplied-title body-tx disposition disp-note-id) - (define title-elems - (cond [(null? supplied-title) (list (default-title (get-elements body-tx)))] - [else (get-elements supplied-title)])) - - (define disposition-part - (cond [(non-empty-string? disposition) - (define-values (mark _) (disposition-values disposition)) - `(a [[class "disposition-mark"] - [href ,(format "~a~a#~a" web-root pagenode disp-note-id)]] - ,mark)] - [else ""])) - ;; Returns a txexpr, the tag will be discarded by the template/snippets - `(title ,@title-elems ,disposition-part)) - -;; Convert a bunch of information about an article into some nice English and links. -(define (make-article-footertext pagenode series disposition disp-note-id note-count) - (define series-part - (match (current-series-title) - [(? non-empty-string? s-title) - (format "<span class=\"series-part\">This is ~a, part of <a href=\"/~a\">‘~a’</a>.</span>" - (current-series-noun) - series - s-title)] - [_ ""])) - (define disp-part - (cond [(non-empty-string? disposition) - (define-values (mark verb) (disposition-values disposition)) - (format "Now considered <a href=\"/~a#~a\">~a</a>." - pagenode - disp-note-id - verb)] - [else ""])) - (define notes-part - (cond [(note-count . > . 1) - (format "There are <a href=\"/~a#furthernotes\">~a notes</a> appended." - pagenode - note-count)] - [(and (note-count . > . 0) (string=? disposition "")) - (format "There is <a href=\"/~a#furthernotes\">a note</a> appended." - pagenode)] - [else ""])) - - (cond [(ormap non-empty-string? (list series-part disp-part notes-part)) - (string-join (list series-part disp-part notes-part))] - [else ""])) - -;; ~~~ Notes ~~~ - -(define (extract-notes pagenode parent-title note-txprs) - (for/list ([n (in-list note-txprs)]) - (make-note n pagenode parent-title))) - -;; Save an individual note to the DB and return the HTML of the complete note as -;; it should appear on an individual article page -(define (make-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) - (>= (length (string-split disposition-attr)) 2)) - (raise-arguments-error 'note - "must be in format \"[symbol] [past-tense-verb]\"" - "disposition attr" - disposition-attr)) - (define-values (disp-mark disp-verb) (disposition-values disposition-attr)) - (let* ([note-id (build-note-id note-tx)] - [title-tx (make-note-title pagenode parent-title-plain)] - [title-html (->html title-tx #:splice? #t)] - [author (maybe-attr 'author attrs default-authorname)] - [author-url (maybe-attr 'author-url attrs)] - [note-srcline (maybe-attr 'srcline attrs)] - [content-html (html$-note-contents disp-mark disp-verb elems)] - [listing-full (html$-note-listing-full pagenode - note-id - title-html - note-date - note-srcline - content-html - author - author-url)]) - (list - (make-cache:note - #:page pagenode - #:html-anchor note-id - #:title-html-flow title-html - #:title-plain (tx-strs title-tx) - #:published note-date - #:author author - #:author-url author-url - #:disposition disposition-attr - #:series-page (current-series-pagenode) - #:conceal (or (maybe-attr 'conceal attrs #f) (maybe-meta 'conceal)) - #:content-html content-html - #:listing-full-html listing-full - #:listing-excerpt-html listing-full - #:listing-short-html "") - (html$-note-in-article note-id note-date content-html author author-url)))) - -(define (make-note-title pagenode parent-title-plain) - `(note-title "Re: " (a [[class "cross-reference"] - [href ,(format "~a~a" web-root pagenode)]] - ,parent-title-plain))) - -;; ~~~ Keyword Index Entries ~~~ - -;; (private) Convert an entry key into a list of at most two elements, -;; a main entry and a sub-entry. -;; "entry" → '("entry" "") -;; "entry!sub" → '("entry" "sub") -;; "entry!sub!why?!? '("entry" "sub") -(define (split-entry str) - (define splits (string-split str "!")) - (list (car splits) - (cadr (append splits (list ""))))) - -(define (index-entry-txpr? tx) - (and (txexpr? tx) - (string=? "index-link" (attr-ref tx 'class "")) ; see definition of html-index - (attr-ref tx 'data-index-entry #f))) - -(define (txexpr->index-entry tx pagenode) - (match (split-entry (attr-ref tx 'data-index-entry)) - [(list main sub) - (make-cache:index-entry - #:entry main - #:subentry sub - #:page pagenode - #:html-anchor (attr-ref tx 'id))])) - -;; Get index entries out of metas -(define (current-metas-keyword-entries pagenode) - (for/list ([kw (in-list (string-split (maybe-meta 'keywords "") #px";\\s*"))]) - (match (split-entry kw) - [(list main sub) - (make-cache:index-entry - #:entry main - #:subentry sub - #:page pagenode - #:html-anchor "")]))) - -;; Save any index entries in doc to the SQLite cache. -;; Sub-entries are specified by "!" in the index key -(define (cache-index-entries! pagenode doc) - (define-values (_ entry-txs) (splitf-txexpr doc index-entry-txpr?)) - (define all-entries - (append (for/list ([etx (in-list entry-txs)]) (txexpr->index-entry etx pagenode)) - (current-metas-keyword-entries pagenode))) - - (delete-index-entries! pagenode) - (save-cache-things! all-entries)) - -(define (cache-index-entries-only! title pagenode doc) - (void - (thread - (lambda () - (call-with-transaction - (cache-conn) - (lambda () - (cache-index-entries! pagenode doc) - (delete-article! pagenode) - (insert-one! (cache-conn) - (make-cache:article - #:title-plain title - #:conceal "blog,feed" - #:page pagenode)))))))) DELETED rss-feed.rkt Index: rss-feed.rkt ================================================================== --- rss-feed.rkt +++ rss-feed.rkt @@ -1,83 +0,0 @@ -#lang racket/base - -; SPDX-License-Identifier: BlueOak-1.0.0 -; This file is licensed under the Blue Oak Model License 1.0.0. - -;; Generates an Atom feed from the SQLite cache - -(require txexpr - deta - racket/match - racket/file - racket/date - racket/string - racket/sequence - "dust.rkt" - "cache.rkt") - -(provide main) - -(define feed-author default-authorname) -(define feed-author-email "joel@jdueck.net") -(define feed-title "The Local Yarn (Beta)") -(define feed-site-url "https://thelocalyarn.com") -(define feed-item-limit 50) - -(define (as-cdata string) - (cdata #f #f (format "<![CDATA[~a]]>" string))) ; cdata from xml package via txexpr - -(define (email-encode str) - (map char->integer (string->list str))) - -;; Atom feeds require dates to be in RFC 3339 format -;; Our published/updated dates only give year-month-day. No need to bother about time zones or DST. -;; So, arbitrarily, everything happens at a quarter of noon UTC. -(define (ymd->rfc3339 datestr) - (format "~aT11:45:00Z" datestr)) - -;; For the feed "updated" value, we do want a complete timestamp. -(define (current-rfc3339) - ;; #f argument to seconds->date forces a UTC timestamp - (define now (seconds->date (* 0.001 (current-inexact-milliseconds)) #f)) - (define timestamp - (parameterize [(date-display-format 'iso-8601)] - (date->string now #t))) - (string-append timestamp "Z")) - -;; Get the data out of the SQLite cache as vectors -(define (fetch-rows) - (sequence->list - (in-entities (cache-conn) - (articles+notes 'content #:series #f #:limit feed-item-limit)))) - -(define (listing->rss-item lst) - (match-define (listing _ path title author published updated html) lst) - (define entry-url (string-append feed-site-url web-root path)) - (define updated-ts (if (non-empty-string? updated) updated published)) - - `(entry (author (name ,author)) - (published ,(ymd->rfc3339 published)) - (updated ,(ymd->rfc3339 updated-ts)) - (title ,title) - (link [[rel "alternate"] [href ,entry-url]]) - (id ,entry-url) - (summary [[type "html"]] - ,(as-cdata html)))) - -(define (rss-feed) - (define feed-xpr - `(feed [[xml:lang "en-us"] [xmlns "http://www.w3.org/2005/Atom"]] - (title ,feed-title) - (link [[rel "self"] [href ,(string-append feed-site-url web-root "feed.xml")]]) - (generator [[uri "http://pollenpub.com/"]] "Pollen") - (id ,(string-append feed-site-url web-root)) - (updated ,(current-rfc3339)) - (author - (name ,feed-author) - (email ,@(email-encode feed-author-email))) - ,@(map listing->rss-item (fetch-rows)))) - (string-append "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" - (xexpr->string feed-xpr))) - -(define (main) - (display-to-file (rss-feed) "feed.xml" #:mode 'text #:exists 'replace)) DELETED series-list.rkt Index: series-list.rkt ================================================================== --- series-list.rkt +++ series-list.rkt @@ -1,34 +0,0 @@ -#lang racket/base - -; SPDX-License-Identifier: BlueOak-1.0.0 -; This file is licensed under the Blue Oak Model License 1.0.0. - -;; Provides fast metadata for series. -;; TO MAKE A NEW SERIES: -;; 1. Create series/my-key.poly.pm -;; 2. Add an entry for my-key to series-list below -;; 3. Use ◊define-meta[series "my-key"] in articles to add them to the series. -;; 4. If ptree-ordered is #t, also create series/my-key.ptree - -(require racket/list) -(struct series (key title noun-plural noun-singular ptree-ordered?)) - -(define series-list - (make-immutable-hash - (list - ;; ------- DEFINE SERIES HERE ----------- - ; Key Title plural noun singular noun phrase - (+series "marquee-fiction" "Marquee Fiction" "Inventions" "an invention" #f) - (+series "local-yarn" "Local Yarn Site Notes" "Project notes" "a project note" #f) - ))) - -(define (series-grouped-list) - (group-by series-noun-plural (hash-values series-list))) - -;; Quick macro to save a little typing -(define-syntax-rule (+series key title plural singular ptree) - (cons key (series key title plural singular ptree))) - -(provide (struct-out series) - series-list - series-grouped-list) DELETED tags-html.rkt Index: tags-html.rkt ================================================================== --- tags-html.rkt +++ tags-html.rkt @@ -1,313 +0,0 @@ -#lang racket/base - -; SPDX-License-Identifier: BlueOak-1.0.0 -; This file is licensed under the Blue Oak Model License 1.0.0. - -;; Tag functions used by pollen.rkt when HTML is the output format. - -(require (for-syntax racket/base racket/syntax)) -(require racket/list - racket/function - racket/draw - racket/class - pollen/decode - pollen/tag - pollen/setup - pollen/core - net/uri-codec - txexpr - "dust.rkt") - -(provide html-fn - html-fndef) - -;; Customized paragraph decoder replaces single newlines within paragraphs -;; with single spaces instead of <br> tags. Allows for “semantic line wrapping”. -(define (decode-hardwrapped-paragraphs xs) - (define (no-linebreaks xs) - (decode-linebreaks xs " ")) - (decode-paragraphs xs #:linebreak-proc no-linebreaks)) - -;; A shortcut macro: lets me define a whole lot of tag functions of the form: -;; (define html-p (default-tag-function 'p) -(define-syntax (provide/define-html-default-tags stx) - (syntax-case stx () - [(_ TAG ...) - (let ([tags (syntax->list #'(TAG ...))]) - (with-syntax ([((HTML-TAG-FUNC HTML-TAG) ...) - (for/list ([htag (in-list tags)]) - (list (format-id stx "html-~a" (syntax-e htag)) (syntax-e htag)))]) - #'(begin - (provide HTML-TAG-FUNC ...) - (define HTML-TAG-FUNC (default-tag-function 'HTML-TAG)) ...)))])) - -;; Here we go: -(provide/define-html-default-tags p - b - strong - i - em - ol - ul - sup - blockquote - code) - -(provide html-root - html-title - html-excerpt - html-excerpt* - html-item - html-section - html-subsection - html-newthought - html-sep - html-caps - html-mono - html-center - html-strike - html-block - html-blockcode - html-index - html-figure - html-figure-@2x - html-image-link - html-dialogue - html-say - html-saylines - html-magick - html-verse - html-attrib - html-link - html-xref - html-url - html-fn - html-fndef - html-note-with-srcline) - -(define html-item (default-tag-function 'li)) -(define html-section (default-tag-function 'h2)) -(define html-subsection (default-tag-function 'h3)) -(define html-newthought (default-tag-function 'span #:class "newthought")) -(define (html-sep) '(hr [[class "sep"]])) -(define html-caps (default-tag-function 'span #:class "caps")) -(define html-center (default-tag-function 'div #:style "text-align: center")) -(define html-strike (default-tag-function 'span #:style "text-decoration: line-through")) -(define html-dialogue (default-tag-function 'dl #:class "dialogue")) -(define html-mono (default-tag-function 'samp)) - -(define (html-block . elements) - `(section [[class "content-block"]] (div [[class "content-block-main"]] ,@elements))) - -(define (html-root . elements) - (invalidate-series) - (define first-pass - (decode-elements (append elements (list (html-footnote-block))) - #:txexpr-elements-proc decode-hardwrapped-paragraphs - #:exclude-tags '(script style figure table pre))) - (define second-pass - (decode-elements first-pass - #:block-txexpr-proc detect-newthoughts - #:inline-txexpr-proc decode-link-urls - #:exclude-tags '(script style pre code))) - `(body ,@second-pass)) - -(define (html-title . elements) `(title ,@elements)) -(define (html-excerpt . elements) `(excerpt ,@elements)) -(define (html-excerpt* . elements) `(excerpt* ,@elements)) - -(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 attrs elems) - (define index-key (maybe-attr 'key attrs (tx-strs `(span ,@elems)))) - `(a [[id ,(here-id (list "_idx-" (uri-encode index-key)))] - [href ,(string-append "/keyword-index.html#" (uri-encode (string-downcase index-key)))] - [data-index-entry ,index-key] - [class "index-link"]] - ,@elems)) - -;; To be used within ◊dialogue -(define (html-say . elems) - `(@ (dt ,(car elems) (span [[class "x"]] ": ")) (dd ,@(cdr elems)))) - -;; Same as ◊say, but preserve linebreaks -(define (html-saylines . elems) - (apply html-say (decode-linebreaks elems))) - -(define (html-verse attrs elems) - (let* ([title (maybe-attr 'title attrs "")] - [italic? (assoc 'italic? attrs)] - [pre-attrs (cond [italic? '([class "verse"] [style "font-style: italic"])] - [else '([class "verse"])])] - [pre-title (cond [(string>? title "") `(p [[class "verse-heading"]] ,title)] - [else ""])]) - `(div [[class "poem"]] ,pre-title (pre ,pre-attrs ,@elems)))) - -(define (html-magick . elems) - (txexpr - 'div '([class "antique"]) - (decode-elements - elems - #:string-proc - (λ (s) (regexp-replace* #px"(?<!f)s(?![fkb\\s”,;.’:\\!\\?]|$)" s "ſ"))))) - -(module+ test - (require rackunit) - ; always round s at the end of a word - (check-equal? (html-magick "mirrors? yes, it is") '(div [[class "antique"]] "mirrors? yes, it is")) - ; always round s before/after f - (check-equal? (html-magick "offset, satisfaction") '(div [[class "antique"]] "offset, ſatisfaction")) - ; always LONG s before hyphen - (check-equal? (html-magick "Shafts-bury") '(div [[class "antique"]] "Shaftſ-bury")) - ; always round s before k or b (17th-century rules) - (check-equal? (html-magick "ask, husband") '(div [[class "antique"]] "ask, husband")) - ; always LONG s everywhere else - (check-equal? (html-magick "song, substitutes") '(div [[class "antique"]] "ſong, ſubſtitutes")) - - ;; Nested elements - (check-equal? - (html-magick '(root "This is " (a [[href "class"]] (b "song, substitutes")))) - '(div [[class "antique"]] (root "This is " (a [[href "class"]] (b "ſong, ſubſtitutes")))))) - -(define html-attrib (default-tag-function 'div #:class "attrib")) - -;; (Private) Get the dimensions of an image file -(define (get-image-size filepath) - (define bmp (make-object bitmap% filepath)) - (list (send bmp get-width) (send bmp get-height))) - -;; (Private) Builds a path to an image in the [image-dir] subfolder of the current document's -;; folder, relative to the current document’s folder -(define (image-source basename) - (define here-path (string->path (maybe-meta 'here-path))) - (define-values (_ here-rel-path-parts) - (drop-common-prefix (explode-path (current-project-root)) - (explode-path here-path))) - (let* ([folder-parts (drop-right here-rel-path-parts 1)] - [img-path-parts (append folder-parts (list images-folder basename))] - [img-path (apply build-path/convention-type 'unix img-path-parts)]) - (path->string img-path))) - -(define (html-figure-@2x . elems) - (define src (image-source (car elems))) - (define alt-text (tx-strs `(span ,@(cdr elems)))) - (define img-width (car (get-image-size (build-path (current-project-root) src)))) - (define style-str (format "width: ~apx" (/ img-width 2.0))) - `(figure (img [[alt ,alt-text] [style ,style-str] [src ,(string-append web-root src)]]) - (figcaption ,@(cdr elems)))) - -(define (html-figure . elems) - (define src (string-append web-root (image-source (car elems)))) - (define alt-text (tx-strs `(span ,@(cdr elems)))) - `(figure [[class "fullwidth"]] - (img [[alt ,alt-text] [src ,src]]) - (figcaption ,@(cdr elems)))) - -;; Simple link to an image -(define (html-image-link . elems) - (define src (image-source (car elems))) - (define title (tx-strs `(span ,@(cdr elems)))) - `(a [[href ,(string-append web-root src)] [title ,title]] ,@(cdr elems))) - -;; There is no way in vanilla CSS to create a selector for “p tags that contain -;; a span of class ‘newthought’”. So we can handle it at the Pollen processing level. -(define (detect-newthoughts block-xpr) - (define (is-newthought? tx) ; Helper function - (and (txexpr? tx) - (eq? 'span (get-tag tx)) - (attrs-have-key? tx 'class) - (string=? "newthought" (attr-ref tx 'class)))) - (if (and (eq? (get-tag block-xpr) 'p) - (is-newthought? (first (get-elements block-xpr)))) - (attr-set block-xpr 'class "pause-before") - block-xpr)) - -;; Links -;; -;; Private use: -(define all-link-urls (make-hash)) - -;; Provided tag functions: -(define (html-link . args) - `(link& [[ref ,(format "~a" (first args))]] ,@(rest args))) - -(define (html-url ref url) - (define page-path (hash-ref (current-metas) 'here-path)) - (define page-link-urls (hash-ref! all-link-urls page-path make-hash)) - (hash-set! page-link-urls (format "~a" ref) url) "") - -;; Private use (by html-root): -(define (decode-link-urls tx) - (define page-path (hash-ref (current-metas) 'here-path)) - (define page-link-urls (hash-ref! all-link-urls page-path make-hash)) - (cond [(eq? (get-tag tx) 'link&) - (let* ([url-ref (attr-ref tx 'ref)] - [url (or (hash-ref page-link-urls url-ref #f) - (format "Missing reference: ~a" url-ref))]) - `(a [[href ,url]] ,@(get-elements tx)))] - [else tx])) - -;; Fast link to another article -(define html-xref - (case-lambda - [(title) `(a [[href ,(format "~aarticles/~a.html" web-root (normalize title))] - [class "xref"]] - (i ,title))] - [elems `(a [[href ,(format "~aarticles/~a.html" web-root (first elems))] - [class "xref"]] - ,@(rest elems))])) - -;; Footnotes -;; -;; Private use: -(define all-fn-names (make-hash)) -(define all-fn-definitions (make-hash)) -(define (fn-id x) (here-id (string-append x "_fn"))) -(define (fndef-id x) (here-id (string-append x "_fndef"))) - -;; Provided footnote tag functions: -(define (html-fn . args) - (define name (format "~a" (first args))) - (define page-path (hash-ref (current-metas) 'here-path)) - (define page-fn-names (cons name (hash-ref! all-fn-names page-path '()))) - (hash-set! all-fn-names page-path page-fn-names) - - (let* ([def-anchorlink (string-append "#" (fndef-id name))] - [nth-ref (number->string (count (curry string=? name) page-fn-names))] - [ref-id (string-append (fn-id name) nth-ref)] - [fn-number (+ 1 (index-of (remove-duplicates (reverse page-fn-names)) name))] - [ref-text (format "(~a)" fn-number)]) - (cond [(empty? (rest args)) `(sup (a [[href ,def-anchorlink] [id ,ref-id]] ,ref-text))] - [else `(span [[class "links-footnote"] [id ,ref-id]] - ,@(rest args) - (sup (a [[href ,def-anchorlink]] ,ref-text)))]))) - -(define (html-fndef . elems) - (define page-path (hash-ref (current-metas) 'here-path)) - (define page-fn-defs (hash-ref! all-fn-definitions page-path make-hash)) - (hash-set! page-fn-defs (format "~a" (first elems)) (rest elems))) - -;; Private use (by html-root) -(define (html-footnote-block) - (define page-path (hash-ref (current-metas) 'here-path)) - (define page-fn-names (hash-ref! all-fn-names page-path '())) - (define page-fn-defs (hash-ref! all-fn-definitions page-path (make-hash))) - (define note-items - (for/list ([fn-name (in-list (remove-duplicates (reverse page-fn-names)))]) - (let* ([definition-text (or (hash-ref page-fn-defs fn-name #f) - '((i "Missing footnote definition!")))] - [backref-count (count (curry string=? fn-name) page-fn-names)] - [backrefs (for/list ([fnref-num (in-range backref-count)]) - `(a [[href ,(string-append "#" - (fn-id fn-name) - (format "~a" (+ 1 fnref-num)))]] "↩"))]) - `(li [[id ,(fndef-id fn-name)]] ,@definition-text ,@backrefs)))) - (cond [(null? note-items) ""] - [else `(section ((class "footnotes")) (hr) (ol ,@note-items))])) - -(define (html-note-with-srcline attrs elems) - (txexpr 'note attrs (decode-hardwrapped-paragraphs elems))) DELETED targets.rkt Index: targets.rkt ================================================================== --- targets.rkt +++ targets.rkt @@ -1,5 +0,0 @@ -#lang racket/base - -(provide targets) - -(define targets '(html)) DELETED util/init.rkt Index: util/init.rkt ================================================================== --- util/init.rkt +++ util/init.rkt @@ -1,15 +0,0 @@ -#lang racket/base - -(require racket/file - pollen/setup - "../cache.rkt" - "../snippets-html.rkt") - -(provide main) - -(define (main) - (init-cache-db!) - - (display-to-file (html$-page-footer) - (build-path (current-project-root) "scribbled" "site-footer.html") - #:exists 'replace)) DELETED web-extra/font.css Index: web-extra/font.css ================================================================== --- web-extra/font.css +++ web-extra/font.css @@ -1,48 +0,0 @@ -/* SPDX-License-Identifier: BlueOak-1.0.0 - This file is licensed under the Blue Oak Model License 1.0.0. -*/ - -@font-face { - font-family: 'Fabiol'; - src: url('LDFabiolPro-Regular.woff2') format('woff2'), - url('LDFabiolPro-Regular.woff') format('woff'); - font-style: normal; - font-weight: 400; -} - -@font-face { - font-family: 'Fabiol'; - src: url('LDFabiolPro-Italic.woff2') format('woff2'), - url('LDFabiolPro-Italic.woff') format('woff'); - font-style: italic; - font-weight: 400; -} - -@font-face { - font-family: 'Fabiol'; - src: url('LDFabiolPro-Bold.woff2') format('woff2'), - url('LDFabiolPro-Bold.woff') format('woff'); - font-style: normal; - font-weight: 700; -} - -@font-face { - font-family: 'Triplicate T4c'; - src: url('Triplicate-T4c.woff') format('woff'); - font-style: normal; - font-weight: 400; - } - -@font-face { - font-family: 'Triplicate T4c'; - src: url('Triplicate-T4c-italic.woff') format('woff'); - font-style: italic; - font-weight: 400; - } - -@font-face { - font-family: 'Triplicate T4c'; - src: url('Triplicate-T4c-bold.woff') format('woff'); - font-style: normal; - font-weight: bold; - } DELETED web-extra/logo.png Index: web-extra/logo.png ================================================================== --- web-extra/logo.png +++ web-extra/logo.png cannot compute difference between binary files DELETED web-extra/mark.svg Index: web-extra/mark.svg ================================================================== --- web-extra/mark.svg +++ web-extra/mark.svg cannot compute difference between binary files DELETED web-extra/martin.css.pp Index: web-extra/martin.css.pp ================================================================== --- web-extra/martin.css.pp +++ web-extra/martin.css.pp @@ -1,1066 +0,0 @@ -#lang pollen/pre - -/* SPDX-License-Identifier: BlueOak-1.0.0 -** This file is licensed under the Blue Oak Model License 1.0.0. */ - -/* Welcome to my CSS File! -** I have named it `martin.css`, after Martin Pale. */ - -◊;{Here, broadly, is the approach we are taking here: - - 1. The site shall look decent and readable even when CSS is unavailable. - - 2. There's a vertical rhythm of ◊x-lineheight[1] that just about - everything follows. - - 3. Define the mobile (smallest screen) layout first... [Lines 026-???] - ...then do some dynamic type sizing on screens 768px+ wide [Lines ???-???] - - 4. If CSS Grid support is detected, we'll do some nice-looking [Lines ???-???] - layout on screens 768px+ wide. -} - -@import url('font.css'); -@import url('normalize.css'); - -/* Let us first address the matter of font size in different screen sizes. */ - -/* Mobile portrait screens will see a minimum 18px font. */ -html { - font-size: 22px; - height: 100%; - } - -/* Start increasing type size dynamically at screen widths of 768px */ -@media (min-width: 768px) { - html { font-size: 2.8vw; } -} - -/* Top out at 23px for screens up to 800px TALL */ -◊; @media only screen and (min-width: 1000px) and (max-height: 800px) { -◊; html { font-size: 26px; } /* = 2.6% of 1000px (min-width) */ -◊; } - -/* Top out at 28px for screens 801px-1000px TALL */ -@media (min-width: 1000px) and (max-height: 920px) { - html { font-size: 28px; } /* = 2.8% of 1000px (min-width) */ -} -/* For screens taller than 1000 px, top out at 32px */ -@media (min-width: 1178px) and (min-height: 921px) { - html { font-size: 33px; } /* = 2.8% of 1178px (min-width) */ -} - -◊; Since line height is used in so many places... -◊(define LINEHEIGHT 1.3) -◊(define lineheight (string-append (number->string LINEHEIGHT) "rem")) -◊(define (x-lineheight multiple) - (string-append (real->decimal-string (* LINEHEIGHT multiple) 2) "rem")) -◊(define (derive-lineheight lines #:per-lines per) - (string-append (real->decimal-string (/ (* LINEHEIGHT per) lines) 3) "rem")) - -◊(define color-bodytext "#2a3d45") ◊; Japanese indigo, baby -◊(define color-pagehead "#a81606") ◊; for Faded gold, a29555 -◊(define color-link "#ab2a23") -◊(define color-linkhover "#c14337") -◊(define color-xrefmark "#c14337") -◊(define color-background "#f7f7f7") -◊(define color-linkbackground "#f7f7f7") - -◊(define body-font "Fabiol, serif") -◊(define mono-font "'Triplicate T4c', monospace") - -◊(define normal-font-features - "\"calt\" on, \"liga\" on, \"clig\" on, \"kern\" on, \"onum\" on, \"pnum\" on") - -◊(define antique-font-features - "\"calt\" on, \"liga\" on, \"clig\" on, \"dlig\" on, \"kern\" on, \"onum\" on, \"pnum\" on") - -/* - **** 1. Mobile-first layout *** - 1.1 All Pages - 1.2 Front page - 1.3 Individual post body markup - 1.4 Journal views (article listings) - */ - -body { - height: 100%; /* height and flex are to keep footer stuck to bottom of page */ - display: flex; - flex-direction: column; - margin: 0; - background: ◊color-background; - - /* Typography: `line-height` is important! - All verticle rhythm based on this value. */ - line-height: ◊lineheight; - font-family: ◊body-font; - - font-feature-settings: ◊normal-font-features; - color: ◊color-bodytext; /* Japanese Indigo, baby */ -} - -/* This is used to hide certain punctuation and things as long as CSS is available. - Turn off CSS and voila: stuff appears. */ -.x { - display: none; -} - -.hist p { - font-variant-alternates: historical-forms; -} -.nonhist { - font-variant-alternates: normal; -} - -main { - background: white; - margin: 0; - padding: ◊x-lineheight[0.5] ◊x-lineheight[1]; - flex: 1 0 auto; -} - -main > a > header { - text-align: center; -} - -main > a > header h1 { - display: none; - /* font-size: ◊x-lineheight[1.5]; - line-height: ◊x-lineheight[2]; - margin: 0 0 ◊x-lineheight[0.5] 0; - font-weight: normal; - font-style: italic; - text-transform: lowercase; - color: ◊color-pagehead; - transition: color 0.25s ease; */ -} - -img.logo { - height: ◊x-lineheight[3]; - filter: none; - transition: filter 0.5s ease; -} - -header:hover img.logo { - filter: invert(12%) sepia(87%) saturate(2559%) hue-rotate(348deg) brightness(125%) contrast(88%); - transition: filter 0.5s ease; -} - -/* I read somewhere years ago that you should specify styling for links in the - following order: Link, Visited, Hover, Active. - → Remember the mnemonic: Lord Vader Has Arrived. - - Not sure if that's relevant advice any more but I still follow it. It can't hurt. */ - -a:link, a:visited { /* [L]ord [V]ader… */ - color: ◊color-link; - text-decoration: none; -} - -a:hover, a:active { /* …[H]as [A]rrived */ - color: ◊color-linkhover; - background: ◊color-linkbackground; -} - -a.index-link:link, a.index-link:visited { - color: ◊color-bodytext; -} - -a.index-link:hover, a.index-link:active { - color: #006400; -} - -a.index-link::after { - content: '\f89b'; - color: #809102; - position: relative; - top: -0.3em; - font-weight: normal; - font-style: normal; -} - -main>a { - color: inherit; -} - -span.links-footnote { - display: inline-block; /* allows keyframe animation to work */ -} - -:target { - animation: hilite 2.5s; -} -@keyframes hilite { - 0% {background: transparent;} - 10% {background: #feffc1;} - 100% {background: transparent;} -} - -main > aside { - text-align: center; -} - -main nav { - font-family: 'Triplicate T4c', monospace; - font-feature-settings: "onum" off; - font-size: 0.7rem; - margin: 0; - margin-top: 0.5rem; - text-align: center; -} - -main nav ul { - list-style-type: none; - margin: 0.2em auto; - padding: 0; -} - -main nav li { - display: none; /* Numbers not displayed on mobile */ - color: gray; -} - -main nav li.nav-text { - display: inline; - text-transform: uppercase; - letter-spacing: 0.05rem; - font-size: 0.6rem; -} - -main nav li.inactive-link { - color: #545454; /* Accessibility (contrast) */ -} - -main nav li.current-page { - color: ◊color-bodytext; - padding: 0.2rem 0.5rem; - border-bottom: dotted ◊color-bodytext 2px; -} - -main nav li a { - padding: 0.2rem 0.5rem; -} - -main nav li a:link, nav li a:visited { - color: ◊color-bodytext; -} - -main nav li a:hover, nav li a:active { - color: ◊color-linkhover; - background: #ebebeb; -} - -i > em { font-style: normal; } - -/* On mobile, an <ARTICLE> is just a box. Later we'll do fancy stuff if grid support is detected. */ -article { - margin: ◊x-lineheight[2] 0 ◊x-lineheight[1] 0; - padding-top: ◊x-lineheight[1]; -} -article:first-of-type { - margin-top: ◊x-lineheight[1]; -} - -/* Here's my thing these days about article titles: they shouldn't be required. When you write in a - paper journal, do you think of a title for every entry? No! You just write the date. The date is - the heading. Makes sense. But, this means I've had to think long and hard about how to present two - different types of articles (those with titles AND dates, and those with just a date for the title). - - filter: invert(12%) sepia(87%) saturate(359%) hue-rotate(348deg) brightness(105%) contrast(88%); - For now: By default, the title-less article is assumed to be the norm. On these, we use <H1> to - show the date in italics. */ -article>h1 { - font-size: ◊x-lineheight[1]; - line-height: ◊x-lineheight[1]; - margin: 0 0 ◊x-lineheight[1] 0; - font-style: italic; - font-weight: bold; -} - -article.no-title>h1 { - font-weight: normal -} - -/* Titles non-bold, non-smallcaps by default. This can be overridden in document markup. */ -h1.entry-title { - margin: 0 0 0 0; - text-transform: none; - font-style: normal; - line-height: 1.7rem; -} - -h1.entry-title.note-full { - font-weight: normal; - font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; -} - -/* This <SPAN> class is used in titles for Notes appended to earlier articles */ -h1.entry-title .cross-reference { - font-feature-settings: "smcp" off, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; - font-style: italic; - text-transform: none; -} - -/* `a.rel-bookmark` is only used in individual article or ‘journal’ views, - not in listings; see the design docs. -*/ -a.rel-bookmark:link, /* Lord */ -a.rel-bookmark:visited { /* Vader */ - text-decoration: none; - background: none; - color: ◊color-bodytext; -} - -a.rel-bookmark:hover, /* Has */ -a.rel-bookmark:active { /* Arrived */ - text-decoration: none; - background: ◊color-linkbackground; - color: #006400; -} - -/* Here's where we add the minty fresh maple leaf glyph. */ -a.rel-bookmark::after { - content: '\f894'; - margin-left: 4px; - font-style: normal; - color: #809102; -} -a.rel-bookmark.note-permlink::after { - content: '\f897'; - margin-left: 4px; -} -div.note a.rel-bookmark.note-permlink::after { - content: ''; -} -div.note a.rel-bookmark.note-permlink::before { - font-family: ◊body-font; - font-size: 1rem; - content: '\00b6'; - margin-left: -0.9rem; - float: left; - margin-top: -2px; -} - -a.rel-bookmark:hover::after { - color: #aaba16; -} - -a.cross-reference:link, -a.cross-reference:visited { - color: ◊color-bodytext; -} - -a.cross-reference::before { - content: '☞\00a0'; /* Non-breaking space */ - font-style: normal; - color: ◊color-xrefmark; -} - -/* Footnote links */ -sup a { - font-weight: bold; - margin-left: 3px; -} - -p.time { - margin: 0 0 ◊x-lineheight[1] 0; -} - -footer.article-info { - margin: ◊x-lineheight[1] 0; - text-align: center; - font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; - color: #555555; /* Accessibility (contrast) */ -} - -footer.article-info::before { - content: "☞\00a0"; -} - -/* Within article info, don’t display series info when on a series page (redundant) */ -body.series-page .series-part { - display: none; -} - -p.time::before { - content: none; -} - -p.time { - font-size: ◊x-lineheight[0.75]; - line-height: ◊x-lineheight[1]; - font-feature-settings: "scmp" off, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; - text-transform: none; - font-style: italic; -} - -article>:last-child::after { - content: '\f88d'; /* Round glyph */ - color: ◊color-bodytext; - font-size: ◊x-lineheight[1]; - line-height: ◊x-lineheight[1]; - text-align: center; - display: block; - margin: ◊x-lineheight[1] 0; -} - -/* Fossil info links */ -.scm-links { - color: #888; - font-size: 0.5rem; - white-space: nowrap; - float: right; - text-align: right; - margin-top: -◊x-lineheight[2.01]; - padding-right: 1ch; - font-family: ◊mono-font; - font-style: italic; -} - -.scm-links a { - display: inline-block; - text-align: center; - width: 0.9rem; - color: #727070; -} - -p { - margin: 0; - text-indent: 0; -} - -p + p { - text-indent: 1em; -} - -.entry-content blockquote, -.content-block-main blockquote { - font-size: ◊x-lineheight[0.7]; - line-height: ◊derive-lineheight[7 #:per-lines 6]; - margin: ◊x-lineheight[1.0] 2em; -} - -.entry-content blockquote:first-child, -.content-block-main blockquote:first-child { - margin-top: 0; -} - -.entry-content blockquote footer, -.content-block-main blockquote footer { - margin-top: ◊x-lineheight[0.5]; - text-align: right; - width: calc(100% + 2em); -} - -.entry-content h2, .content-block-main h2 { - font-size: 1rem; - font-weight: normal; - font-feature-settings: "smcp" on; - text-transform: lowercase; - text-align: left; - padding-top: 0.2rem; - border-top: dotted #ccc 2px; - color: #666; - line-height: ◊x-lineheight[0.96]; -} - -@media(min-width: 667px) { - .entry-content h2, .content-block-main h2 { - float: left; - margin-left: -8rem; - text-align: right; - width: 7rem; - margin-top: -0.25rem; - } -} - -hr.sep { - border: 0; - background: none; - margin: ◊x-lineheight[1] 0; - height: ◊x-lineheight[1]; -} - -hr.sep:after { - display: block; - text-align: center; - content: '❧'; - font-size: ◊x-lineheight[1]; -} - -.caps, span.smallcaps, span.newthought { - font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; - font-style: normal; -} - -.caps { - text-transform: lowercase; - font-size: 1.1em; -} - -p.pause-before { - margin-top: ◊x-lineheight[2]; - text-indent: 0; -} - -.entry-content ul, -.entry-content ol, -.content-block-main ul, -.content-block-main ol { - margin: ◊x-lineheight[0.5] 0 ◊x-lineheight[0.5] 0.6rem; - padding: 0; -} - -.entry-content li, -.content-block-main li { - margin: 0 0 ◊x-lineheight[0.5] 0; - padding: 0; - text-indent: 0; -} - -.entry-content ul, -.content-block-main ul { - list-style: none; -} - -.entry-content ul li:before, -.content-block-main ul li:before { - content: '•'; - margin-left: -0.4rem; - margin-right: 0.2rem; -} - -code { - font-size: 0.75rem; - font-family: ◊mono-font; - background: #ddd; - border-radius: 0.2em; -} - -pre { - line-height: ◊derive-lineheight[7 #:per-lines 6]; - max-width: 100%; - overflow-x: auto; - tab-size: 4; -} - -pre code { - border: 0; - background: none; - font-size: 0.6rem; -} - -pre.code { - border: dotted #aaa 2px; - padding-left: 0.2em; - line-height: 0.7rem; -} - -samp { - font-family: ◊mono-font; - font-size: 0.7rem; -} - -pre.verse { - font-family: ◊body-font; - font-size: 1rem; - line-height: ◊x-lineheight[1]; - width: auto; - margin: ◊x-lineheight[1] auto; - display: table; - white-space: pre-wrap; - /* Whitespace is preserved by the browser. - Text will wrap when necessary, and on line breaks */ -} - -p.verse-heading { - font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; - text-align: center; - font-size: 1.3rem; -} - -div.attrib { - text-align: right; - font-size: .8rem; - margin-top: -◊x-lineheight[0.5]; - margin-bottom: ◊x-lineheight[1]; -} - -.entry-content figure, -.content-block-main figure { - margin: ◊x-lineheight[1] 0; - padding: 0; -} - -figure>a { - margin: 0; - padding: 0; - font-family: arial, sans-serif; -} -figure img { - max-width: 100%; - margin: 0 auto; -} - -figcaption { - font-size: 0.8rem; - line-height: ◊derive-lineheight[4 #:per-lines 3]; - margin-bottom: 0.3rem; - text-align: left; -} - -dl { - margin: ◊x-lineheight[1] 0; -} - -dl.dialogue dt { - font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; - font-style: normal; - text-transform: lowercase; -} - -section.entry-content p.signoff { - margin-top: ◊x-lineheight[1]; - font-size: 1.2rem; -} - -section.footnotes { - margin-top: ◊x-lineheight[1]; - font-size: 0.9rem; - line-height: ◊derive-lineheight[7 #:per-lines 6]; -} - -section.footnotes hr { - border: 0; - background: ◊color-bodytext; - height: 1px; - margin: 0; - width: 75%; - text-align: left; -} - -section.footnotes ol { - margin: ◊x-lineheight[0.5] 0 0 0; -} - - -p.further-reading { - margin-top: ◊x-lineheight[1]; - text-indent: 0; - background: #eee; - padding-left: 0.3rem; - border-radius: 3px; -} - -p.further-reading:hover { - background-color: #ddd; -} - -p.further-reading a { - color: ◊color-bodytext !important; - font-style: italic; -} - -p.further-reading a:hover, p.further-reading a:active { - background-color: #ddd; -} - -/* ******* “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; - - color: ◊color-bodytext; - margin-top: 0; - float: none; - text-align: left; -} - -div.note h3 { - margin-top: 0; - font-size: 1rem; - font-weight: normal; - font-family: ◊mono-font; - font-size: 0.7rem; - background: #f3f3f3; - border-top: solid #b6b6b6 1px; - padding-left: 0.2rem; - margin-bottom: 0.3rem; -} - -div.note-meta { - margin-top: ◊x-lineheight[1]; - color: #888; - font-size: 1.2rem; -} - -.by-proprietor .note-meta { - display: none; -} - -div.note + div.note { - margin-top: ◊x-lineheight[2]; -} - -.disposition-mark { - color: ◊color-xrefmark; - position: relative; - top: -0.5em; - font-size: 0.83em; -} - -.disposition-mark-in-note { - background: ◊color-xrefmark; - color: white; - border-radius: 0.08em; - padding: 0 0.1em; - margin: 0 0.4em 0 0; - text-decoration: none !important; -} - -/* ******* (Mobile first) “Short” list styling ******* - */ - -section.content-block { -} - -article.short-listing { - margin: ◊x-lineheight[1] 0; - padding: 0; - display: block; - border: none; - box-shadow: none; -} - -article.short-listing a { - display: block; -} - -article.short-listing time { - color: #999; -} - -article.short-listing h3 { - font-size: 1.2rem; - margin: 0; - padding: 0; - font-weight: normal; -} - -/* ******* (Mobile first) Columnar series list styling ******* - */ - -.column-list { - margin: 0; - column-count: 2; - column-width: 16ch; - column-gap: 1ch; -} - -@media (min-width: 667px) { - .column-list { - column-count: auto; - margin: 0 auto; - max-width: 70ch; - } -} - -.column-list div { - padding-left: 0.25em; /* Keeps some italic descenders inside the box */ - text-align: left; - break-inside: avoid; -} - -.column-list h2 { - font-feature-settings: "smcp" on; - text-transform: lowercase; - font-weight: normal; - font-size: 1em; - margin: 0; -} - -.column-list ul { - margin-top: 0; - list-style-type: none; - padding: 0; - margin-bottom: 0.5rem; -} - -/* ******* (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; -} - -#keywordindex ul ul { - margin-left: 0.5em; - font-size: smaller; -} - -/* Footer ***** */ - -footer#main { - text-align: left; - font-size: 0.8rem; - line-height: 1rem; - width: 90%; - margin: 0 auto; - flex-shrink: 0; -} - -@media (min-width: 667px) { - footer#main { - text-align: center; - width: calc(100% - 4vw); - padding: 0 2vw 1rem 2vw; - background: linear-gradient(◊color-background 5%, #dedede 100%); - } -} - -footer#main p.title { - font-size: 1rem; - font-feature-settings: "smcp" on; - text-transform: lowercase; - margin-top: ◊x-lineheight[2]; - margin-bottom: ◊x-lineheight[0.5]; -} - -footer#main nav { - font-feature-settings: "smcp" on; - text-transform: lowercase; -} - -footer#main nav code { - font-size: 0.6rem; -} - - -/* End of mobile-first typography and layout */ - - -/* Here’s where we start getting funky for any viewport wider than mobile portrait. - An iPhone 6 is 667px wide in landscape mode, so that’s our breakpoint. */ - -@media (min-width: 667px) { - main { - margin: 0 auto; - padding-left: 1rem; - padding-right: 1rem; - width: 30rem; - max-width: 90%; - } - - main nav li { display: inline; } /* Display page numbers on larger screens */ - - @supports (grid-area: auto) { - main { - width: 42rem; - background: none; - } - - header img.logo { - height: ◊x-lineheight[3]; - max-height: 103px; - width: auto; - } - - /* This header is display:none above, so the following is vestigial */ - main header h1 { - grid-area: masthead; - margin: 0; - line-height: 1em; - } - - article { - display: grid; - grid-template-columns: 8rem 7fr 1fr; - grid-template-rows: auto auto auto; - grid-template-areas: - "corner title title" - "margin main rightmargin"; - align-items: start; /* Puts everything at the top */ - margin-bottom: 0; - grid-column-gap: 1rem; - box-shadow: 0.4em 0.4em 10px #e3e3e3; - background: white; - border: solid 1px #dedede; - border-radius: 2px; - } - - article>h1 { - grid-area: margin; - font-size: 1.1rem; - text-align: right; - } - - article>h1.entry-title { - grid-area: title; - font-size: ◊x-lineheight[1]; - text-align: left; - padding-right: 0; - margin-bottom: 0.9rem; - - } - - p.time { - grid-area: corner; - text-align: right; - font-size: 1.1rem; - line-height: 1.7rem; - } - - footer.article-info { - padding-top: 0.2rem; - grid-area: margin; - text-align: right; - font-size: 0.8rem; - line-height: ◊derive-lineheight[4 #:per-lines 3]; - margin: 0; - padding-left: 3px; - } - - article.no-title footer.article-info { - margin-top: 1.5rem; - padding-top: 0; - } - - .scm-links { - float: none; - grid-area: rightmargin; - height: 100%; - } - - article.with-title .scm-links { - grid-area: title; - } - - .scm-links a:hover { - color: #2176ff; - background: none; - } - - section.entry-content { - grid-area: main; - /* Prevent content from overriding grid column sizing */ - /* See https://css-tricks.com/preventing-a-grid-blowout/ */ - min-width: 0; - } - - section.entry-content > :first-child { - margin-top: 0 !important; - } - - article>:last-child::after { - content: none; - margin 0; - display: none; - } - - section.entry-content::after { - content: '\f88d'; - color: ◊color-bodytext; - font-size: ◊x-lineheight[1]; - line-height: ◊x-lineheight[1]; - text-align: center; - display: block; - margin: ◊x-lineheight[1] 0; - } - - section.entry-content figure { - display: grid; - width: calc(112.5% + 8rem); - margin-left: -8rem; - grid-template-columns: 7rem 1fr; - grid-column-gap: 1rem; - grid-template-areas: "margin main"; - } - - section.entry-content figure img { - grid-area: main; - } - 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 { - } - - /* ******* (Grid support) Journal View styling ******* - */ - - section.content-block { - display: grid; - grid-template-columns: 8rem 7fr 1fr; - grid-template-rows: auto auto auto; - grid-template-areas: "margin main ."; - align-items: start; /* Puts everything at the top */ - margin-bottom: 0; - grid-column-gap: 1rem; - box-shadow: 0.4em 0.4em 10px #e3e3e3; - background: white; - border: solid 1px #dedede; - border-radius: 2px; - } - div.content-block-main { - padding-top: ◊x-lineheight[1]; - padding-bottom: ◊x-lineheight[1]; - grid-area: main; - } - - div.content-block-main > :first-child { - margin-top: 0 !important; - } - } -} - -@media print { - html { font-size: 13pt; } - body { background: white; color: black; } - main { width: 100%; } - footer.article-info { color: black; } - article, section.content-block { - box-shadow: none; - border: none; - } - a:link, a:visited, a:hover, a:active { - color: black; - border-bottom: dotted 1px black; - } - footer#main { display: none; } -} DELETED web-extra/normalize.css Index: web-extra/normalize.css ================================================================== --- web-extra/normalize.css +++ web-extra/normalize.css @@ -1,341 +0,0 @@ -/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ - -/* Document - ========================================================================== */ - -/** - * 1. Correct the line height in all browsers. - * 2. Prevent adjustments of font size after orientation changes in iOS. - */ - -html { - line-height: 1.15; /* 1 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/* Sections - ========================================================================== */ - -/** - * Remove the margin in all browsers. - */ - -body { - margin: 0; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -pre { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/* Text-level semantics - ========================================================================== */ - -/** - * Remove the gray background on active links in IE 10. - */ - -a { - background-color: transparent; -} - -/** - * 1. Remove the bottom border in Chrome 57- - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10. - */ - -img { - border-style: none; -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change the font styles in all browsers. - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font-family: inherit; /* 1 */ - font-size: 100%; /* 1 */ - line-height: 1.15; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * Correct the inability to style clickable types in iOS and Safari. - */ - -button, -[type="button"], -[type="reset"], -[type="submit"] { - -webkit-appearance: button; -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Correct the padding in Firefox. - */ - -fieldset { - padding: 0.35em 0.75em 0.625em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Remove the default vertical scrollbar in IE 10+. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10. - * 2. Remove the padding in IE 10. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding in Chrome and Safari on macOS. - */ - -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/* Interactive - ========================================================================== */ - -/* - * Add the correct display in Edge, IE 10+, and Firefox. - */ - -details { - display: block; -} - -/* - * Add the correct display in all browsers. - */ - -summary { - display: list-item; -} - -/* Misc - ========================================================================== */ - -/** - * Add the correct display in IE 10+. - */ - -template { - display: none; -} - -/** - * Add the correct display in IE 10. - */ - -[hidden] { - display: none; -} ADDED web/font.css Index: web/font.css ================================================================== --- web/font.css +++ web/font.css @@ -0,0 +1,48 @@ +/* SPDX-License-Identifier: BlueOak-1.0.0 + This file is licensed under the Blue Oak Model License 1.0.0. +*/ + +@font-face { + font-family: 'Fabiol'; + src: url('LDFabiolPro-Regular.woff2') format('woff2'), + url('LDFabiolPro-Regular.woff') format('woff'); + font-style: normal; + font-weight: 400; +} + +@font-face { + font-family: 'Fabiol'; + src: url('LDFabiolPro-Italic.woff2') format('woff2'), + url('LDFabiolPro-Italic.woff') format('woff'); + font-style: italic; + font-weight: 400; +} + +@font-face { + font-family: 'Fabiol'; + src: url('LDFabiolPro-Bold.woff2') format('woff2'), + url('LDFabiolPro-Bold.woff') format('woff'); + font-style: normal; + font-weight: 700; +} + +@font-face { + font-family: 'Triplicate T4c'; + src: url('Triplicate-T4c.woff') format('woff'); + font-style: normal; + font-weight: 400; + } + +@font-face { + font-family: 'Triplicate T4c'; + src: url('Triplicate-T4c-italic.woff') format('woff'); + font-style: italic; + font-weight: 400; + } + +@font-face { + font-family: 'Triplicate T4c'; + src: url('Triplicate-T4c-bold.woff') format('woff'); + font-style: normal; + font-weight: bold; + } ADDED web/logo.png Index: web/logo.png ================================================================== --- web/logo.png +++ web/logo.png cannot compute difference between binary files ADDED web/mark.svg Index: web/mark.svg ================================================================== --- web/mark.svg +++ web/mark.svg cannot compute difference between binary files ADDED web/martin.css.pp Index: web/martin.css.pp ================================================================== --- web/martin.css.pp +++ web/martin.css.pp @@ -0,0 +1,1066 @@ +#lang pollen/pre + +/* SPDX-License-Identifier: BlueOak-1.0.0 +** This file is licensed under the Blue Oak Model License 1.0.0. */ + +/* Welcome to my CSS File! +** I have named it `martin.css`, after Martin Pale. */ + +◊;{Here, broadly, is the approach we are taking here: + + 1. The site shall look decent and readable even when CSS is unavailable. + + 2. There's a vertical rhythm of ◊x-lineheight[1] that just about + everything follows. + + 3. Define the mobile (smallest screen) layout first... [Lines 026-???] + ...then do some dynamic type sizing on screens 768px+ wide [Lines ???-???] + + 4. If CSS Grid support is detected, we'll do some nice-looking [Lines ???-???] + layout on screens 768px+ wide. +} + +@import url('font.css'); +@import url('normalize.css'); + +/* Let us first address the matter of font size in different screen sizes. */ + +/* Mobile portrait screens will see a minimum 18px font. */ +html { + font-size: 22px; + height: 100%; + } + +/* Start increasing type size dynamically at screen widths of 768px */ +@media (min-width: 768px) { + html { font-size: 2.8vw; } +} + +/* Top out at 23px for screens up to 800px TALL */ +◊; @media only screen and (min-width: 1000px) and (max-height: 800px) { +◊; html { font-size: 26px; } /* = 2.6% of 1000px (min-width) */ +◊; } + +/* Top out at 28px for screens 801px-1000px TALL */ +@media (min-width: 1000px) and (max-height: 920px) { + html { font-size: 28px; } /* = 2.8% of 1000px (min-width) */ +} +/* For screens taller than 1000 px, top out at 32px */ +@media (min-width: 1178px) and (min-height: 921px) { + html { font-size: 33px; } /* = 2.8% of 1178px (min-width) */ +} + +◊; Since line height is used in so many places... +◊(define LINEHEIGHT 1.3) +◊(define lineheight (string-append (number->string LINEHEIGHT) "rem")) +◊(define (x-lineheight multiple) + (string-append (real->decimal-string (* LINEHEIGHT multiple) 2) "rem")) +◊(define (derive-lineheight lines #:per-lines per) + (string-append (real->decimal-string (/ (* LINEHEIGHT per) lines) 3) "rem")) + +◊(define color-bodytext "#2a3d45") ◊; Japanese indigo, baby +◊(define color-pagehead "#a81606") ◊; for Faded gold, a29555 +◊(define color-link "#ab2a23") +◊(define color-linkhover "#c14337") +◊(define color-xrefmark "#c14337") +◊(define color-background "#f7f7f7") +◊(define color-linkbackground "#f7f7f7") + +◊(define body-font "Fabiol, serif") +◊(define mono-font "'Triplicate T4c', monospace") + +◊(define normal-font-features + "\"calt\" on, \"liga\" on, \"clig\" on, \"kern\" on, \"onum\" on, \"pnum\" on") + +◊(define antique-font-features + "\"calt\" on, \"liga\" on, \"clig\" on, \"dlig\" on, \"kern\" on, \"onum\" on, \"pnum\" on") + +/* + **** 1. Mobile-first layout *** + 1.1 All Pages + 1.2 Front page + 1.3 Individual post body markup + 1.4 Journal views (article listings) + */ + +body { + height: 100%; /* height and flex are to keep footer stuck to bottom of page */ + display: flex; + flex-direction: column; + margin: 0; + background: ◊color-background; + + /* Typography: `line-height` is important! + All verticle rhythm based on this value. */ + line-height: ◊lineheight; + font-family: ◊body-font; + + font-feature-settings: ◊normal-font-features; + color: ◊color-bodytext; /* Japanese Indigo, baby */ +} + +/* This is used to hide certain punctuation and things as long as CSS is available. + Turn off CSS and voila: stuff appears. */ +.x { + display: none; +} + +.hist p { + font-variant-alternates: historical-forms; +} +.nonhist { + font-variant-alternates: normal; +} + +main { + background: white; + margin: 0; + padding: ◊x-lineheight[0.5] ◊x-lineheight[1]; + flex: 1 0 auto; +} + +main > a > header { + text-align: center; +} + +main > a > header h1 { + display: none; + /* font-size: ◊x-lineheight[1.5]; + line-height: ◊x-lineheight[2]; + margin: 0 0 ◊x-lineheight[0.5] 0; + font-weight: normal; + font-style: italic; + text-transform: lowercase; + color: ◊color-pagehead; + transition: color 0.25s ease; */ +} + +img.logo { + height: ◊x-lineheight[3]; + filter: none; + transition: filter 0.5s ease; +} + +header:hover img.logo { + filter: invert(12%) sepia(87%) saturate(2559%) hue-rotate(348deg) brightness(125%) contrast(88%); + transition: filter 0.5s ease; +} + +/* I read somewhere years ago that you should specify styling for links in the + following order: Link, Visited, Hover, Active. + → Remember the mnemonic: Lord Vader Has Arrived. + + Not sure if that's relevant advice any more but I still follow it. It can't hurt. */ + +a:link, a:visited { /* [L]ord [V]ader… */ + color: ◊color-link; + text-decoration: none; +} + +a:hover, a:active { /* …[H]as [A]rrived */ + color: ◊color-linkhover; + background: ◊color-linkbackground; +} + +a.index-link:link, a.index-link:visited { + color: ◊color-bodytext; +} + +a.index-link:hover, a.index-link:active { + color: #006400; +} + +a.index-link::after { + content: '\f89b'; + color: #809102; + position: relative; + top: -0.3em; + font-weight: normal; + font-style: normal; +} + +main>a { + color: inherit; +} + +span.links-footnote { + display: inline-block; /* allows keyframe animation to work */ +} + +:target { + animation: hilite 2.5s; +} +@keyframes hilite { + 0% {background: transparent;} + 10% {background: #feffc1;} + 100% {background: transparent;} +} + +main > aside { + text-align: center; +} + +main nav { + font-family: 'Triplicate T4c', monospace; + font-feature-settings: "onum" off; + font-size: 0.7rem; + margin: 0; + margin-top: 0.5rem; + text-align: center; +} + +main nav ul { + list-style-type: none; + margin: 0.2em auto; + padding: 0; +} + +main nav li { + display: none; /* Numbers not displayed on mobile */ + color: gray; +} + +main nav li.nav-text { + display: inline; + text-transform: uppercase; + letter-spacing: 0.05rem; + font-size: 0.6rem; +} + +main nav li.inactive-link { + color: #545454; /* Accessibility (contrast) */ +} + +main nav li.current-page { + color: ◊color-bodytext; + padding: 0.2rem 0.5rem; + border-bottom: dotted ◊color-bodytext 2px; +} + +main nav li a { + padding: 0.2rem 0.5rem; +} + +main nav li a:link, nav li a:visited { + color: ◊color-bodytext; +} + +main nav li a:hover, nav li a:active { + color: ◊color-linkhover; + background: #ebebeb; +} + +i > em { font-style: normal; } + +/* On mobile, an <ARTICLE> is just a box. Later we'll do fancy stuff if grid support is detected. */ +article { + margin: ◊x-lineheight[2] 0 ◊x-lineheight[1] 0; + padding-top: ◊x-lineheight[1]; +} +article:first-of-type { + margin-top: ◊x-lineheight[1]; +} + +/* Here's my thing these days about article titles: they shouldn't be required. When you write in a + paper journal, do you think of a title for every entry? No! You just write the date. The date is + the heading. Makes sense. But, this means I've had to think long and hard about how to present two + different types of articles (those with titles AND dates, and those with just a date for the title). + + filter: invert(12%) sepia(87%) saturate(359%) hue-rotate(348deg) brightness(105%) contrast(88%); + For now: By default, the title-less article is assumed to be the norm. On these, we use <H1> to + show the date in italics. */ +article>h1 { + font-size: ◊x-lineheight[1]; + line-height: ◊x-lineheight[1]; + margin: 0 0 ◊x-lineheight[1] 0; + font-style: italic; + font-weight: bold; +} + +article.no-title>h1 { + font-weight: normal +} + +/* Titles non-bold, non-smallcaps by default. This can be overridden in document markup. */ +h1.entry-title { + margin: 0 0 0 0; + text-transform: none; + font-style: normal; + line-height: 1.7rem; +} + +h1.entry-title.note-full { + font-weight: normal; + font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; +} + +/* This <SPAN> class is used in titles for Notes appended to earlier articles */ +h1.entry-title .cross-reference { + font-feature-settings: "smcp" off, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; + font-style: italic; + text-transform: none; +} + +/* `a.rel-bookmark` is only used in individual article or ‘journal’ views, + not in listings; see the design docs. +*/ +a.rel-bookmark:link, /* Lord */ +a.rel-bookmark:visited { /* Vader */ + text-decoration: none; + background: none; + color: ◊color-bodytext; +} + +a.rel-bookmark:hover, /* Has */ +a.rel-bookmark:active { /* Arrived */ + text-decoration: none; + background: ◊color-linkbackground; + color: #006400; +} + +/* Here's where we add the minty fresh maple leaf glyph. */ +a.rel-bookmark::after { + content: '\f894'; + margin-left: 4px; + font-style: normal; + color: #809102; +} +a.rel-bookmark.note-permlink::after { + content: '\f897'; + margin-left: 4px; +} +div.note a.rel-bookmark.note-permlink::after { + content: ''; +} +div.note a.rel-bookmark.note-permlink::before { + font-family: ◊body-font; + font-size: 1rem; + content: '\00b6'; + margin-left: -0.9rem; + float: left; + margin-top: -2px; +} + +a.rel-bookmark:hover::after { + color: #aaba16; +} + +a.cross-reference:link, +a.cross-reference:visited { + color: ◊color-bodytext; +} + +a.cross-reference::before { + content: '☞\00a0'; /* Non-breaking space */ + font-style: normal; + color: ◊color-xrefmark; +} + +/* Footnote links */ +sup a { + font-weight: bold; + margin-left: 3px; +} + +p.time { + margin: 0 0 ◊x-lineheight[1] 0; +} + +footer.article-info { + margin: ◊x-lineheight[1] 0; + text-align: center; + font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; + color: #555555; /* Accessibility (contrast) */ +} + +footer.article-info::before { + content: "☞\00a0"; +} + +/* Within article info, don’t display series info when on a series page (redundant) */ +body.series-page .series-part { + display: none; +} + +p.time::before { + content: none; +} + +p.time { + font-size: ◊x-lineheight[0.75]; + line-height: ◊x-lineheight[1]; + font-feature-settings: "scmp" off, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; + text-transform: none; + font-style: italic; +} + +article>:last-child::after { + content: '\f88d'; /* Round glyph */ + color: ◊color-bodytext; + font-size: ◊x-lineheight[1]; + line-height: ◊x-lineheight[1]; + text-align: center; + display: block; + margin: ◊x-lineheight[1] 0; +} + +/* Fossil info links */ +.scm-links { + color: #888; + font-size: 0.5rem; + white-space: nowrap; + float: right; + text-align: right; + margin-top: -◊x-lineheight[2.01]; + padding-right: 1ch; + font-family: ◊mono-font; + font-style: italic; +} + +.scm-links a { + display: inline-block; + text-align: center; + width: 0.9rem; + color: #727070; +} + +p { + margin: 0; + text-indent: 0; +} + +p + p { + text-indent: 1em; +} + +.entry-content blockquote, +.content-block-main blockquote { + font-size: ◊x-lineheight[0.7]; + line-height: ◊derive-lineheight[7 #:per-lines 6]; + margin: ◊x-lineheight[1.0] 2em; +} + +.entry-content blockquote:first-child, +.content-block-main blockquote:first-child { + margin-top: 0; +} + +.entry-content blockquote footer, +.content-block-main blockquote footer { + margin-top: ◊x-lineheight[0.5]; + text-align: right; + width: calc(100% + 2em); +} + +.entry-content h2, .content-block-main h2 { + font-size: 1rem; + font-weight: normal; + font-feature-settings: "smcp" on; + text-transform: lowercase; + text-align: left; + padding-top: 0.2rem; + border-top: dotted #ccc 2px; + color: #666; + line-height: ◊x-lineheight[0.96]; +} + +@media(min-width: 667px) { + .entry-content h2, .content-block-main h2 { + float: left; + margin-left: -8rem; + text-align: right; + width: 7rem; + margin-top: -0.25rem; + } +} + +hr.sep { + border: 0; + background: none; + margin: ◊x-lineheight[1] 0; + height: ◊x-lineheight[1]; +} + +hr.sep:after { + display: block; + text-align: center; + content: '❧'; + font-size: ◊x-lineheight[1]; +} + +.caps, span.smallcaps, span.newthought { + font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; + font-style: normal; +} + +.caps { + text-transform: lowercase; + font-size: 1.1em; +} + +p.pause-before { + margin-top: ◊x-lineheight[2]; + text-indent: 0; +} + +.entry-content ul, +.entry-content ol, +.content-block-main ul, +.content-block-main ol { + margin: ◊x-lineheight[0.5] 0 ◊x-lineheight[0.5] 0.6rem; + padding: 0; +} + +.entry-content li, +.content-block-main li { + margin: 0 0 ◊x-lineheight[0.5] 0; + padding: 0; + text-indent: 0; +} + +.entry-content ul, +.content-block-main ul { + list-style: none; +} + +.entry-content ul li:before, +.content-block-main ul li:before { + content: '•'; + margin-left: -0.4rem; + margin-right: 0.2rem; +} + +code { + font-size: 0.75rem; + font-family: ◊mono-font; + background: #ddd; + border-radius: 0.2em; +} + +pre { + line-height: ◊derive-lineheight[7 #:per-lines 6]; + max-width: 100%; + overflow-x: auto; + tab-size: 4; +} + +pre code { + border: 0; + background: none; + font-size: 0.6rem; +} + +pre.code { + border: dotted #aaa 2px; + padding-left: 0.2em; + line-height: 0.7rem; +} + +samp { + font-family: ◊mono-font; + font-size: 0.7rem; +} + +pre.verse { + font-family: ◊body-font; + font-size: 1rem; + line-height: ◊x-lineheight[1]; + width: auto; + margin: ◊x-lineheight[1] auto; + display: table; + white-space: pre-wrap; + /* Whitespace is preserved by the browser. + Text will wrap when necessary, and on line breaks */ +} + +p.verse-heading { + font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; + text-align: center; + font-size: 1.3rem; +} + +div.attrib { + text-align: right; + font-size: .8rem; + margin-top: -◊x-lineheight[0.5]; + margin-bottom: ◊x-lineheight[1]; +} + +.entry-content figure, +.content-block-main figure { + margin: ◊x-lineheight[1] 0; + padding: 0; +} + +figure>a { + margin: 0; + padding: 0; + font-family: arial, sans-serif; +} +figure img { + max-width: 100%; + margin: 0 auto; +} + +figcaption { + font-size: 0.8rem; + line-height: ◊derive-lineheight[4 #:per-lines 3]; + margin-bottom: 0.3rem; + text-align: left; +} + +dl { + margin: ◊x-lineheight[1] 0; +} + +dl.dialogue dt { + font-feature-settings: "smcp" on, "liga" on, "clig" on, "dlig" on, "kern" on, "onum" on, "pnum" on; + font-style: normal; + text-transform: lowercase; +} + +section.entry-content p.signoff { + margin-top: ◊x-lineheight[1]; + font-size: 1.2rem; +} + +section.footnotes { + margin-top: ◊x-lineheight[1]; + font-size: 0.9rem; + line-height: ◊derive-lineheight[7 #:per-lines 6]; +} + +section.footnotes hr { + border: 0; + background: ◊color-bodytext; + height: 1px; + margin: 0; + width: 75%; + text-align: left; +} + +section.footnotes ol { + margin: ◊x-lineheight[0.5] 0 0 0; +} + + +p.further-reading { + margin-top: ◊x-lineheight[1]; + text-indent: 0; + background: #eee; + padding-left: 0.3rem; + border-radius: 3px; +} + +p.further-reading:hover { + background-color: #ddd; +} + +p.further-reading a { + color: ◊color-bodytext !important; + font-style: italic; +} + +p.further-reading a:hover, p.further-reading a:active { + background-color: #ddd; +} + +/* ******* “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; + + color: ◊color-bodytext; + margin-top: 0; + float: none; + text-align: left; +} + +div.note h3 { + margin-top: 0; + font-size: 1rem; + font-weight: normal; + font-family: ◊mono-font; + font-size: 0.7rem; + background: #f3f3f3; + border-top: solid #b6b6b6 1px; + padding-left: 0.2rem; + margin-bottom: 0.3rem; +} + +div.note-meta { + margin-top: ◊x-lineheight[1]; + color: #888; + font-size: 1.2rem; +} + +.by-proprietor .note-meta { + display: none; +} + +div.note + div.note { + margin-top: ◊x-lineheight[2]; +} + +.disposition-mark { + color: ◊color-xrefmark; + position: relative; + top: -0.5em; + font-size: 0.83em; +} + +.disposition-mark-in-note { + background: ◊color-xrefmark; + color: white; + border-radius: 0.08em; + padding: 0 0.1em; + margin: 0 0.4em 0 0; + text-decoration: none !important; +} + +/* ******* (Mobile first) “Short” list styling ******* + */ + +section.content-block { +} + +article.short-listing { + margin: ◊x-lineheight[1] 0; + padding: 0; + display: block; + border: none; + box-shadow: none; +} + +article.short-listing a { + display: block; +} + +article.short-listing time { + color: #999; +} + +article.short-listing h3 { + font-size: 1.2rem; + margin: 0; + padding: 0; + font-weight: normal; +} + +/* ******* (Mobile first) Columnar series list styling ******* + */ + +.column-list { + margin: 0; + column-count: 2; + column-width: 16ch; + column-gap: 1ch; +} + +@media (min-width: 667px) { + .column-list { + column-count: auto; + margin: 0 auto; + max-width: 70ch; + } +} + +.column-list div { + padding-left: 0.25em; /* Keeps some italic descenders inside the box */ + text-align: left; + break-inside: avoid; +} + +.column-list h2 { + font-feature-settings: "smcp" on; + text-transform: lowercase; + font-weight: normal; + font-size: 1em; + margin: 0; +} + +.column-list ul { + margin-top: 0; + list-style-type: none; + padding: 0; + margin-bottom: 0.5rem; +} + +/* ******* (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; +} + +#keywordindex ul ul { + margin-left: 0.5em; + font-size: smaller; +} + +/* Footer ***** */ + +footer#main { + text-align: left; + font-size: 0.8rem; + line-height: 1rem; + width: 90%; + margin: 0 auto; + flex-shrink: 0; +} + +@media (min-width: 667px) { + footer#main { + text-align: center; + width: calc(100% - 4vw); + padding: 0 2vw 1rem 2vw; + background: linear-gradient(◊color-background 5%, #dedede 100%); + } +} + +footer#main p.title { + font-size: 1rem; + font-feature-settings: "smcp" on; + text-transform: lowercase; + margin-top: ◊x-lineheight[2]; + margin-bottom: ◊x-lineheight[0.5]; +} + +footer#main nav { + font-feature-settings: "smcp" on; + text-transform: lowercase; +} + +footer#main nav code { + font-size: 0.6rem; +} + + +/* End of mobile-first typography and layout */ + + +/* Here’s where we start getting funky for any viewport wider than mobile portrait. + An iPhone 6 is 667px wide in landscape mode, so that’s our breakpoint. */ + +@media (min-width: 667px) { + main { + margin: 0 auto; + padding-left: 1rem; + padding-right: 1rem; + width: 30rem; + max-width: 90%; + } + + main nav li { display: inline; } /* Display page numbers on larger screens */ + + @supports (grid-area: auto) { + main { + width: 42rem; + background: none; + } + + header img.logo { + height: ◊x-lineheight[3]; + max-height: 103px; + width: auto; + } + + /* This header is display:none above, so the following is vestigial */ + main header h1 { + grid-area: masthead; + margin: 0; + line-height: 1em; + } + + article { + display: grid; + grid-template-columns: 8rem 7fr 1fr; + grid-template-rows: auto auto auto; + grid-template-areas: + "corner title title" + "margin main rightmargin"; + align-items: start; /* Puts everything at the top */ + margin-bottom: 0; + grid-column-gap: 1rem; + box-shadow: 0.4em 0.4em 10px #e3e3e3; + background: white; + border: solid 1px #dedede; + border-radius: 2px; + } + + article>h1 { + grid-area: margin; + font-size: 1.1rem; + text-align: right; + } + + article>h1.entry-title { + grid-area: title; + font-size: ◊x-lineheight[1]; + text-align: left; + padding-right: 0; + margin-bottom: 0.9rem; + + } + + p.time { + grid-area: corner; + text-align: right; + font-size: 1.1rem; + line-height: 1.7rem; + } + + footer.article-info { + padding-top: 0.2rem; + grid-area: margin; + text-align: right; + font-size: 0.8rem; + line-height: ◊derive-lineheight[4 #:per-lines 3]; + margin: 0; + padding-left: 3px; + } + + article.no-title footer.article-info { + margin-top: 1.5rem; + padding-top: 0; + } + + .scm-links { + float: none; + grid-area: rightmargin; + height: 100%; + } + + article.with-title .scm-links { + grid-area: title; + } + + .scm-links a:hover { + color: #2176ff; + background: none; + } + + section.entry-content { + grid-area: main; + /* Prevent content from overriding grid column sizing */ + /* See https://css-tricks.com/preventing-a-grid-blowout/ */ + min-width: 0; + } + + section.entry-content > :first-child { + margin-top: 0 !important; + } + + article>:last-child::after { + content: none; + margin 0; + display: none; + } + + section.entry-content::after { + content: '\f88d'; + color: ◊color-bodytext; + font-size: ◊x-lineheight[1]; + line-height: ◊x-lineheight[1]; + text-align: center; + display: block; + margin: ◊x-lineheight[1] 0; + } + + section.entry-content figure { + display: grid; + width: calc(112.5% + 8rem); + margin-left: -8rem; + grid-template-columns: 7rem 1fr; + grid-column-gap: 1rem; + grid-template-areas: "margin main"; + } + + section.entry-content figure img { + grid-area: main; + } + 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 { + } + + /* ******* (Grid support) Journal View styling ******* + */ + + section.content-block { + display: grid; + grid-template-columns: 8rem 7fr 1fr; + grid-template-rows: auto auto auto; + grid-template-areas: "margin main ."; + align-items: start; /* Puts everything at the top */ + margin-bottom: 0; + grid-column-gap: 1rem; + box-shadow: 0.4em 0.4em 10px #e3e3e3; + background: white; + border: solid 1px #dedede; + border-radius: 2px; + } + div.content-block-main { + padding-top: ◊x-lineheight[1]; + padding-bottom: ◊x-lineheight[1]; + grid-area: main; + } + + div.content-block-main > :first-child { + margin-top: 0 !important; + } + } +} + +@media print { + html { font-size: 13pt; } + body { background: white; color: black; } + main { width: 100%; } + footer.article-info { color: black; } + article, section.content-block { + box-shadow: none; + border: none; + } + a:link, a:visited, a:hover, a:active { + color: black; + border-bottom: dotted 1px black; + } + footer#main { display: none; } +} ADDED web/normalize.css Index: web/normalize.css ================================================================== --- web/normalize.css +++ web/normalize.css @@ -0,0 +1,341 @@ +/*! normalize.css v8.0.0 | MIT License | github.com/necolas/normalize.css */ + +/* Document + ========================================================================== */ + +/** + * 1. Correct the line height in all browsers. + * 2. Prevent adjustments of font size after orientation changes in iOS. + */ + +html { + line-height: 1.15; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ +} + +/* Sections + ========================================================================== */ + +/** + * Remove the margin in all browsers. + */ + +body { + margin: 0; +} + +/** + * Correct the font size and margin on `h1` elements within `section` and + * `article` contexts in Chrome, Firefox, and Safari. + */ + +h1 { + font-size: 2em; + margin: 0.67em 0; +} + +/* Grouping content + ========================================================================== */ + +/** + * 1. Add the correct box sizing in Firefox. + * 2. Show the overflow in Edge and IE. + */ + +hr { + box-sizing: content-box; /* 1 */ + height: 0; /* 1 */ + overflow: visible; /* 2 */ +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +pre { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/* Text-level semantics + ========================================================================== */ + +/** + * Remove the gray background on active links in IE 10. + */ + +a { + background-color: transparent; +} + +/** + * 1. Remove the bottom border in Chrome 57- + * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. + */ + +abbr[title] { + border-bottom: none; /* 1 */ + text-decoration: underline; /* 2 */ + text-decoration: underline dotted; /* 2 */ +} + +/** + * Add the correct font weight in Chrome, Edge, and Safari. + */ + +b, +strong { + font-weight: bolder; +} + +/** + * 1. Correct the inheritance and scaling of font size in all browsers. + * 2. Correct the odd `em` font sizing in all browsers. + */ + +code, +kbd, +samp { + font-family: monospace, monospace; /* 1 */ + font-size: 1em; /* 2 */ +} + +/** + * Add the correct font size in all browsers. + */ + +small { + font-size: 80%; +} + +/** + * Prevent `sub` and `sup` elements from affecting the line height in + * all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sub { + bottom: -0.25em; +} + +sup { + top: -0.5em; +} + +/* Embedded content + ========================================================================== */ + +/** + * Remove the border on images inside links in IE 10. + */ + +img { + border-style: none; +} + +/* Forms + ========================================================================== */ + +/** + * 1. Change the font styles in all browsers. + * 2. Remove the margin in Firefox and Safari. + */ + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 1 */ + line-height: 1.15; /* 1 */ + margin: 0; /* 2 */ +} + +/** + * Show the overflow in IE. + * 1. Show the overflow in Edge. + */ + +button, +input { /* 1 */ + overflow: visible; +} + +/** + * Remove the inheritance of text transform in Edge, Firefox, and IE. + * 1. Remove the inheritance of text transform in Firefox. + */ + +button, +select { /* 1 */ + text-transform: none; +} + +/** + * Correct the inability to style clickable types in iOS and Safari. + */ + +button, +[type="button"], +[type="reset"], +[type="submit"] { + -webkit-appearance: button; +} + +/** + * Remove the inner border and padding in Firefox. + */ + +button::-moz-focus-inner, +[type="button"]::-moz-focus-inner, +[type="reset"]::-moz-focus-inner, +[type="submit"]::-moz-focus-inner { + border-style: none; + padding: 0; +} + +/** + * Restore the focus styles unset by the previous rule. + */ + +button:-moz-focusring, +[type="button"]:-moz-focusring, +[type="reset"]:-moz-focusring, +[type="submit"]:-moz-focusring { + outline: 1px dotted ButtonText; +} + +/** + * Correct the padding in Firefox. + */ + +fieldset { + padding: 0.35em 0.75em 0.625em; +} + +/** + * 1. Correct the text wrapping in Edge and IE. + * 2. Correct the color inheritance from `fieldset` elements in IE. + * 3. Remove the padding so developers are not caught out when they zero out + * `fieldset` elements in all browsers. + */ + +legend { + box-sizing: border-box; /* 1 */ + color: inherit; /* 2 */ + display: table; /* 1 */ + max-width: 100%; /* 1 */ + padding: 0; /* 3 */ + white-space: normal; /* 1 */ +} + +/** + * Add the correct vertical alignment in Chrome, Firefox, and Opera. + */ + +progress { + vertical-align: baseline; +} + +/** + * Remove the default vertical scrollbar in IE 10+. + */ + +textarea { + overflow: auto; +} + +/** + * 1. Add the correct box sizing in IE 10. + * 2. Remove the padding in IE 10. + */ + +[type="checkbox"], +[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/** + * Correct the cursor style of increment and decrement buttons in Chrome. + */ + +[type="number"]::-webkit-inner-spin-button, +[type="number"]::-webkit-outer-spin-button { + height: auto; +} + +/** + * 1. Correct the odd appearance in Chrome and Safari. + * 2. Correct the outline style in Safari. + */ + +[type="search"] { + -webkit-appearance: textfield; /* 1 */ + outline-offset: -2px; /* 2 */ +} + +/** + * Remove the inner padding in Chrome and Safari on macOS. + */ + +[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/** + * 1. Correct the inability to style clickable types in iOS and Safari. + * 2. Change font properties to `inherit` in Safari. + */ + +::-webkit-file-upload-button { + -webkit-appearance: button; /* 1 */ + font: inherit; /* 2 */ +} + +/* Interactive + ========================================================================== */ + +/* + * Add the correct display in Edge, IE 10+, and Firefox. + */ + +details { + display: block; +} + +/* + * Add the correct display in all browsers. + */ + +summary { + display: list-item; +} + +/* Misc + ========================================================================== */ + +/** + * Add the correct display in IE 10+. + */ + +template { + display: none; +} + +/** + * Add the correct display in IE 10. + */ + +[hidden] { + display: none; +} ADDED yarn-doc/cache.scrbl Index: yarn-doc/cache.scrbl ================================================================== --- yarn-doc/cache.scrbl +++ yarn-doc/cache.scrbl @@ -0,0 +1,246 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt" scribble/example) + +@(require (for-label deta + db + racket/base + racket/contract + sugar/coerce + pollen/template + "../dust.rkt" + "../crystalize.rkt" + "../cache.rkt")) + +@(define example-eval (make-base-eval)) +@(example-eval '(require "cache.rkt" txexpr)) + +@title[#:tag "cache-rkt"]{Cache} + +@defmodule["cache.rkt" #:packages ()] + +In this project there are several places – the blog, the footer on each page, the RSS feed, series +pages — where data from an amorphous group of Pollen documents is needed. This is what the cache is +for. + +This module defines and provides the schema and database connection to the SQLite cache, and some +functions for retrieving records from the cache. Use these when you need quick access to pre-cooked +HTML. + +@section{Cache database} + +@defparam[cache-conn conn connection?]{ +The database connection. +} + +@defproc[(init-cache-db!) void?]{ + +Creates and initializes the SQLite database cache file (named @filepath{vitreous.sqlite} and located +in the project root folder) by running queries to create tables in the database if they do not +exist. + +} + +@section{Retrieving cached data} + +Some of this looks a little wacky, but it’s a case of putting a little extra complextity into the +back end to make things simple on the front end. These functions are most commonly used inside the +@emph{body} of a Pollen document (i.e., series pages). + +@filebox["series/my-series.poly.pm" +@codeblock|{ +#lang pollen + +◊title{My New Series} + +...some other content + +◊fenced-listing[(articles+notes 'excerpt #:order 'asc)] +}| +] + +@defproc[(fenced-listing [query query?]) txexpr?]{ + +Fetches a the HTML strings from the SQLite cache and returns a @racket['style] tagged X-expression +with these strings as its elements. The @racket[_query] will usually be the result of a call to +@racket[articles] or @racket[articles+notes], but can be any custom query that projects onto the +@racket[listing] schema (see @racket[project-onto]). + +The reason for enclosing the results in a @racket['style] txexpr is to prevent the HTML from being +escaped by @racket[->html] in the template. This tag was picked for the job because there will +generally never be a need to include any actual CSS information inside a @tt{<style>} tag in any +page, so it can be safely filtered out later. To remove the enclosing @tt{<style>} tag, see +@racket[unfence]. + +} + +@defproc[(listing-htmls [listing-query query?]) (listof string?)]{ + +Returns the HTML bodies for the articles and/or notes returned by @racket[_listing-query] as a list +of strings. The @racket[_query] will usually be the result of a call to @racket[articles] or +@racket[articles+notes], but can be any custom query that projects onto the @racket[listing] schema +(see @racket[project-onto]). + +} + +@deftogether[(@defproc[(articles [type (or/c 'full 'excerpt 'short 'content)] + [#:series series (or/c string? (listof string?) boolean?) #t] + [#:limit limit integer? -1] + [order stringish? 'desc]) query?] + @defproc[(articles+notes [type (or/c 'full 'excerpt 'short 'content)] + [#:series series (or/c string? (listof string?) boolean?) #t] + [#:limit limit integer? -1] + [order stringish? 'desc]) query?])]{ + +Create a query that fetches either articles only, or articles and notes intermingled, respectively. +The results will be sorted by publish date according to @racket[_order] and optionally limited to +a particular series. Use the resulting query with the @racket[listing-htmls] or +@racket[fenced-listing] functions provided by this module, or with deta’s @racket[in-entities] if +you want to work with the @racket[listing] schema structs. + +The @racket[_type] parameter specifies what version of the articles’ and notes’ HTML markup you +want. For HTML suitable for listing several articles and/or notes together on the same page, use +@racket['full] (the full content but not including @tech{notes}), @racket['excerpt] (like full but +abbreviated to only the excerpt if one was specified) or @racket['short] (date and title only). Use +@racket['content] to get the entire HTML content, including any notes but not including any header +or footer. (This is the option used in the RSS feed.) + +If @racket[_series] expression evaluates to @racket[#f], articles will not be filtered by series. If +it evaluates to @racket[#t] (the default), articles will be filtered by those that specify the +current output of @racket[here-output-path] in their @tt{series_pagenode} column in the SQLite +cache. If a string or a symbol is supplied, articles will be filtered by those containing the result +of @racket[(format "series/~a.html" _series)] in their @tt{series_pagenode} column in the SQLite +cache. If a list of strings or symbols is provided, this @racket[format] operation will be applied +to each of its members and articles whose @tt{series_pagenode} column matches any of the resulting +values will be included. + +The @racket[_order] expression must evaluate to either @racket["ASC"] or @racket["DESC"] (or +equivalent symbols) and the @racket[_limit] expressions must evaluate to a value suitable for use in +the @tt{LIMIT} clause of @ext-link["https://sqlite.org/lang_select.html"]{a SQLite @tt{SELECT} +statement}. An expression that evaluates to a negative integer (the default) is the same as having +no limit. + +Typically you will pass these functions by name to listing functions like @racket[fenced-listing] +rather than calling them directly. + +@examples[#:eval example-eval +(articles 'full)] +} + +@defproc[(unfence [html string?]) string?]{ + +Returns @racket[_html] with all occurrences of @racket["<style>"] and @racket["</style>"] removed. +The contents of the style tags are left intact. + +Use this in templates with strings returned from @racket[->html] when called on docs that use the +@racket[fenced-listing] tag function. + +} + +@section{Modifying the cache} + +@defproc[(save-cache-things! + [things (listof (or/c cache:article? cache:note? cache:index-entry?))]) void?]{ + +Saves all the @racket[_thing]s to the cache database. + +} + +@deftogether[(@defproc[(delete-article! [page stringish?]) void?] + @defproc[(delete-notes! [page stringish?]) void?])]{ + +Delete a particular article, or all notes for a particular article, respectively. + +} + +@section{Schema} + +The cache database has four tables: @tt{articles}, @tt{notes}, @tt{index_entries} and @tt{series}. +Each of these has a corresponding schema, shown below. In addition, there is a “virtual” schema, +@tt{listing}, for use with queries which may or may not combine articles and notes intermingled. + +The work of picking apart an article’s exported @tt{doc} and @tt{metas} into rows in these tables is +done by @racket[parse-and-cache-article!]. + +The below are shown as @code{struct} forms but are actually defined with deta’s +@racket[define-schema]. Each schema has an associated struct with the same name and a smart +constructor called @tt{make-@emph{id}}. The struct’s “dumb” constructor is hidden so that invalid +entities cannot be created. For every defined field there is an associated functional setter and +updater named @tt{set-@emph{id}-field} and @tt{update-@emph{id}-field}, respectively. + +@defstruct*[cache:article ([id id/f] + [page symbol/f] + [title-plain string/f] + [title-html-flow string/f] + [title-specified boolean/f] + [published string/f] + [updated string/f] + [author string/f] + [conceal string/f] + [series-page string/f] + [noun-singular string/f] + [note-count integer/f] + [content-html string/f] + [disposition string/f] + [disp-html-anchor string/f] + [listing-full-html string/f] + [listing-excerpt-html string/f] + [listing-short-html string/f]) + #:constructor-name make-cache:article]{ + +Table holding cached @tech{article} information. + +When creating a @racket[cache:article] (should you ever need to do so directly, which is unlikely), +the only required fields are @racket[_page], @racket[_title], and @racket[_conceal]. + +} + +@defstruct*[cache:note ([id id/f] + [page symbol/f] + [html-anchor string/f] + [title-html-flow string/f] + [title-plain string/f] + [author string/f] + [author-url string/f] + [published string/f] + [disposition string/f] + [content-html string/f] + [series-page symbol/f] + [conceal string/f] + [listing-full-html string/f] + [listing-excerpt-html string/f] + [listing-short-html string/f]) + #:constructor-name make-cache:note]{ + +Table holding cached information on @tech{notes}. + +} + +@defstruct*[cache:index-entry ([id id/f] + [entry string/f] + [subentry string/f] + [page symbol/f] + [html-anchor string/f]) + #:constructor-name make-cache:index-entry]{ + +Table holding cached information about index entries found in @tech{articles}. + +} + +@defstruct*[listing ([path string/f] + [title string/f] + [author string/f] + [published string/f] + [updated string/f] + [html string/f]) + #:constructor-name make-listing]{ + +This is a “virtual” schema targeted by @racket[articles] and @racket[articles+notes] using deta’s +@racket[project-onto]. It supplies the minimum set of fields needed to build the RSS feed, and which +are common to both articles and notes; most times (e.g., on @tech{series} pages) only the @tt{html} +field is used, via @racket[fenced-listing] or @racket[listing-htmls]. + +} ADDED yarn-doc/crystalize.scrbl Index: yarn-doc/crystalize.scrbl ================================================================== --- yarn-doc/crystalize.scrbl +++ yarn-doc/crystalize.scrbl @@ -0,0 +1,61 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt") + +@(require (for-label "../pollen.rkt" + "../crystalize.rkt" + "../cache.rkt" + racket/base + racket/contract + racket/string + txexpr + pollen/core + pollen/pagetree)) + +@title[#:tag "crystalize-rkt"]{Crystalize} + +@defmodule["crystalize.rkt" #:packages ()] + +“Crystalizing” is an extra layer in between docs and templates that destructures the @tt{doc} and +stores it in various pieces in a SQLite cache. Individual articles save chunks of rendered HTML to +the cache when their individual pages are rendered. When pulling together listings of articles in +different contexts that need to be filtered and sorted, a SQL query is much faster than trolling +through the Pollen cache for matching docs and regenerating the HTML. + +@margin-note{These functions are designed to be used within templates, so that the rows in the cache +database for a page are updated right when that web page is rendered.} + +@defproc[(parse-and-cache-article! [pagenode pagenode?] [doc txexpr?]) + (values non-empty-string? non-empty-string?)]{ + +Returns two values: the “plain” title of the article, and a string containing the full HTML of +@racket[_doc], in that order. + +The title is returned separately for use in the HTML @tt{<title>} tag. If the @racket[_doc] doesn’t +specify a title, a provisional title is constructed using @racket[default-title]. + +Privately, it does a lot of other work. The article is analyzed, additional metadata is constructed +and saved to the SQLite cache and saved using @racket[make-cache:article]. If the article specifies +a @racket['series] meta, information about that series is fetched and used in the rendering of the +article. If there are @racket[note]s or @racket[index] tags in the doc, they are parsed and saved +individually to the SQLite cache (using @racket[make-cache:note] and +@racket[make-cache:index-entry]). If any of the notes use the @code{#:disposition} attribute, +information about the disposition is parsed out and used in the rendering of the article. + +} + +@defproc[(cache-index-entries-only! [title string?] [page pagenode?] [doc txexpr?]) void?]{ + +Saves only the @racket[index] entres in @racket[_doc] to the cache database, so that they appear in +the keyword index. + +This function allows pages that are not @tech{articles} to have their own keyword index entries, and +should be used in the templates for such pages. + +As a side effect of calling this function, a minimal @racket[cache:article] is created for the page +with its @racket['conceal] meta set to @racket{all}, to exclude it from any listings. + +} ADDED yarn-doc/design.scrbl Index: yarn-doc/design.scrbl ================================================================== --- yarn-doc/design.scrbl +++ yarn-doc/design.scrbl @@ -0,0 +1,172 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt" + racket/runtime-path + (for-label "../pollen.rkt")) + +@(require (for-label racket/base)) + +@title{Basic Notions} + +@section[#:tag "design-goals"]{Design Goals} + +The design of @italic{The Local Yarn} is guided by requirements that have evolved since I started +the site in 1999. I enumerate them here because they explain why the code is necessarily more +complicated than a typical blog: + +@itemlist[ + @item{@bold{The writing will publish to two places from the same source: the web server, and the + bookshelf.} The web server, because it’s a fun, fast way to publish writing and code to the whole + world (you knew that already); but also on bookshelves, because + @ext-link["https://thelocalyarn.com/excursus/secretary/posts/web-books.html"]{a web server is like + a projector}, and I want to be able to turn it off someday and still have something to show for all + my work. Plus, I just like printed books.} + + @item{@bold{Changes are part of the content.} I like to revisit, resurface and amend things I’ve + written before. Views change, new ideas come along. In a typical blog the focus is always at + whatever’s happening at the head of the time stream; an addendum to an older post is, for all + practical purposes, invisible and nearly useless. I want every published edit to an article to be + findable and linkable. I want addenda to be extremely visible. These addenda should also be able to + mark major shifts in the author’s own perspective on what they originally wrote.} + + @item{@bold{The system will gracefully accomodate experimentation.} I should be able to write + fiction, poetry, opinion pieces, minor observations or collections, or anything else, and have or + create a good home for it here.} + + @item{@bold{Everything produced here should look good.}} + + @item{@bold{Reward exploration without disorienting the reader.} Draw connections between related + thoughts using typographic conventions and organizational devices that would be familiar to + a reader of books. Where dissimilar writings appear together, place signals that help the reader + understand what they are looking at, switch contexts, and find more if they wish.} + + @item{@bold{Everything is produced, and reproducible, by an automatable process.} No clicking or + tapping around in GUI apps to publish web pages and books.} + + ] + +@section{Names for things and how they fit together} + +The Local Yarn is mostly comprised of @tech{articles} (individual writings) which may contain +@tech{notes} (addenda by the author or others) and may also be grouped into @tech{series}. These are +similar to a typical blog’s @italic{posts}, @italic{comments} and @italic{categories}, but there are +important differences. + +@subsection{Articles} + +The @deftech{article} is the basic unit of content, like a typical blog post. In the web edition, +each article has its own @tt{.html} file; in print editions, an article may comprise either +a chapter or a part of a chapter, depending on the content. + +An article can start out very small — just a date and a few sentences. @bold{Supplying a title is +optional.} Later, it may grow in any of several directions: @tech{notes} can be added, or a title, or +cross-references to later articles; or it may be added to a series. Or it may just remain the way it +started. + +@subsection{Notes} + +A @deftech{note} is a comment or addendum to an @tech{article} using the @racket[note] tag. It may +be written by the same person who wrote the article, or submitted by a reader. + +@(define-runtime-path diagram-notes "diagram-notes.png") +@centered{@responsive-retina-image[diagram-notes]} + +As shown above, a note appears at the bottom of the article to which it is attached, but it also +appears in the blog and in the RSS feed as a separate piece of content, and is given the same visual +weight as actual articles. + +A note may optionally have a @deftech{disposition} which reflects a change in attitude towards its +parent article. A disposition consists of a @italic{disposition mark} such as an asterisk or dagger, +and a past-tense verb. For example, an author may revisit an opinion piece written years earlier and +add a note describing how their opinion has changed; the tag for this note might include +@racket[#:disposition "* recanted"] as an attribute. This would cause the @tt{*} to be added to the +article’s title, and the phrase “Now considered recanted” to be added to the margin, with a link to +the note. + +@subsubsection{Notes vs. blog “comments”} + +Typical blog comments serve as kind of a temporary discussion spot for a few days or weeks after +a post is published. Commenting on an old post feels useless because the comment is only visible at +the bottom of its parent post, and older posts are never “bumped” back into visibility. + +By contrast, notes on @italic{The Local Yarn} appear as self-contained writings at the top of the +blog and RSS feed as soon as they are published. This “resurfaces” the original article +to which they are attached. This extra visibility also makes them a good tool for the original +author to fill out or update the article. In effect, with notes, each article potentially becomes +its own miniature blog. + +The flip side of this change is that what used to be the “comment section” is no longer allowed to +function as a kind of per-article chat. + +@tabular[#:sep @hspace[1] + #:style 'boxed + #:row-properties '((bottom-border top)) + (list + (list @bold{Typical Blog Comments} @bold{Local Yarn @emph{Notes}}) + (list "Rarely used after a post has aged" + "Commonly used on posts many years old") + (list "Visible only at the bottom of the parent post" + "Included in the main stream of posts and in the RSS feed alongside actual posts") + (list "Invites any and all feedback, from small compliments to lengthy rebuttals" + "Readers invited to treat their responses as submissions to a publication.") + (list "Usually used by readers" + "Usually used by the original author") + (list "Don’t affect the original post" + "May have properties (e.g. disposition) that change the status and +presentation of the original post") + (list "Moderation (if done) is typically binary: approved or not" + "Moderation may take the form of edits and inline responses."))] + +@subsection{Series} + +A @deftech{series} is a grouping of @tech{articles} into a particular order under a descriptive +title. A series may present its own written content alongside the listing of its articles. + +The page for a series can choose how to display its articles: chronologically, or in an arbitrary +order. It can display articles only, or a mixed listing of articles and @tech{notes}, like the blog. +And it can choose to display articles in list form, or as excerpts, or in their entirety. + +A series can specify @italic{nouns} (noun phrases, really) to be applied to its articles. So, for +example, a series of forceful opinion pieces might designate its articles as @emph{naked +aspirations}; the phrase “This is a naked aspiration, part of the series @italic{My Uncensored +Thoughts}” would appear prominently in the margins. Likewise, a time-ordered series of observations +might call its articles “journal entries”. + +It will be easy for any series to become a printed @emph{book}, using the techniques I +demonstrated in +@ext-link["https://thelocalyarn.com/excursus/secretary/posts/web-books.html"]{@italic{The Unbearable +Lightness of Web Pages}}, and in @other-doc['(lib "bookcover/scribblings/bookcover.scrbl")]. + +@subsubsection{Series vs. blog “categories”} + +Typical blogs are not very good at presenting content that may vary a lot in subject, length and +style. The kind of writing I want to experiment with may change a lot from day to day, season to +season, decade to decade. I wanted a single system that could organize extremely varied kinds of +writings and present them in a thoughtful, coherent way, rather than starting a new blog every time +I wanted to try writing a different kind of thing. + +My solution to this was to enrich the idea of “categories”. Rather than being simply labels that you +slap on blog posts, they would be titled collections with their own unique content and way of +presenting articles and notes. In addition, they could pass down certain properties to the posts +they contain, that can be used to give signals to the reader about what they are looking at. + +@tabular[#:sep @hspace[1] + #:style 'boxed + #:row-properties '((bottom-border top)) + (list + (list @bold{Typical Blog Categories/Tags} @bold{Local Yarn @emph{Series}}) + (list "Every article needs to have one" + "Many or most articles won’t have one") + (list "Named with a single word" + "Named with a descriptive title") + (list "Has no content or properties of its own" + "Has its own written content, and properties such as nouns, ordering, etc.") + (list "Broad in scope, few in number" + "Narrow in scope, many in number") + (list "Selected to be relevant for use across the entire lifetime of the site" + "Selected without reference to future creative direction; may be closed after only + a few articles"))] + ADDED yarn-doc/dust.scrbl Index: yarn-doc/dust.scrbl ================================================================== --- yarn-doc/dust.scrbl +++ yarn-doc/dust.scrbl @@ -0,0 +1,287 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt" + scribble/example) + +@(require (for-label "../pollen.rkt" + "../dust.rkt" + "../cache.rkt" + "../series-list.rkt" + racket/base + racket/contract + txexpr + sugar/coerce + pollen/tag + pollen/setup + pollen/pagetree + pollen/core)) + +@(define dust-eval (make-base-eval)) +@(dust-eval '(require "dust.rkt" txexpr)) + +@title{Dust} + +@defmodule["dust.rkt" #:packages ()] + +This is where I put constants and helper functions that are needed pretty much everywhere in the +project. In a simpler project these would go in @seclink["pollen-rkt"]{@filepath{pollen.rkt}} but +here I have other modules sitting “behind” that one in the @tt{require} chain. + +@section{Constants} + +@defthing[default-authorname string? #:value "Joel Dueck"] + +Used as the default author name for @code{note}s, and (possibly in the future) for articles +generally. + +@defthing[web-root path-string? #:value "/"] + +Specifies the path between the domain name and the root folder of the website generated by this +project. + +@deftogether[(@defthing[articles-folder path-string? #:value "articles"] + @defthing[series-folder path-string? #:value "series"])] + +The names of the folders that contain the Pollen source documents for Articles and Series +respectively, relative to the project’s document root. + +@defthing[images-folder path-string? #:value "images"] + +The name of the subfolders within @racket[articles-folder] and @racket[series-folder] used for +holding image files. + +@deftogether[(@defproc[(articles-pagetree) pagetree?] + @defproc[(series-pagetree) pagetree?])] + +These are project-wide pagetrees: @racket[articles-pagetree] contains a pagenode for every Pollen +document contained in @racket[articles-folder], and @racket[series-pagetree] contains a pagenode for +every Pollen document in @racket[series-folder]. The pagenodes themselves point to the rendered +@tt{.html} targets of the source documents. + +@deftogether[(@defproc[(here-output-path) path?] + @defproc[(here-source-path) path?])]{ + +Returns the path to the current output or source file, relative to @racket[current-project-root]. If +no metas are available, returns @racket[(string->path ".")]. + +For the output path, this is similar to the @tt{here} variable that Pollen provides, except it is +available outside templates. As to the source path, Pollen provides it via the @racket['here-path] +key in the current metas, but it is a full absolute path, rather then relative to +@racket[current-project-root]. + +} + +@defproc[(checked-in?) boolean?]{ + +Returns @racket[#t] if the current article is checked into the Fossil repo, @racket[#f] otherwise. + +} + +@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, the hash of @racket[(string->path ".")] is used. 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] + +Find the value of @racket[_key] in the supplied list of attributes, returning the value of +@racket[_missing-expr] if it’s not there. + +I had to write this because @racket[attr-ref] wants a whole tagged X-expression (not just the +attributes); also, by default it raises an exception when @racket[_key] is missing, rather than +returning an empty string. + +@defproc[(maybe-meta [key symbolish?] [missing-expr any/c ""]) any/c] + +Look up a value in @code{(current-metas)} that may or may not be present, returning the value of +@racket[_missing-expr] if it’s not there. + +@defproc[(tx-strs [tx txexpr?]) string?] + +Finds all the strings from the @emph{elements} of @racket[_tx] (ignoring attributes) and +concatenates them together. + +@examples[#:eval dust-eval +(tx-strs '(p [[class "intro"]] + (em "I’m not opening the safe") ", Wilson remembers thinking."))] + +@defproc[(make-tag-predicate [sym symbol?] ...) (-> any/c boolean?)] + +Returns a function (or @italic{predicate}) that returns @racket[#t] if its argument is +a @racket[_txexpr] whose tag matches any @racket[_sym]. This predicate is useful for passing as the +@racket[_pred] expression in functions @racket[splitf-txexpr] and @racket[findf-txexpr]. + +@examples[#:eval dust-eval +(define is-aside? (make-tag-predicate 'aside 'sidebar)) + +(is-aside? '(q "I am not mad, Sir Topas. I say to you this house is dark.")) +(is-aside? '(aside "How smart a lash that speech doth give my Conscience?")) +(is-aside? '(sidebar "Many copies that we use today are conflated texts."))] + +@defproc[(first-words [txprs (listof txexpr?)] [n exact-nonnegative-integer?]) string?] + +Given a list of tagged X-expressions, returns a string containing the first @racket[_n] words found +in the string elements of @racket[_txprs], or all of the words if there are less than @racket[_n] +words available. Used by @racket[default-title]. + +This function aims to be smart about punctuation, and equally fast no matter how large the list of +elements that you send it. + +@examples[#:eval dust-eval +(define txs-decimals + '((p "Four score and 7.8 years ago — our fathers etc etc"))) +(define txs-punc-and-split-elems + '((p "“Stop!” she called.") (p "(She was never one to be silent.)"))) +(define txs-dashes + '((p [[class "newthought"]] (span [[class "smallcaps"]] "One - and") " only one.") + (p "That was all she would allow."))) +(define txs-parens-commas + '((p "She counted (" (em "one, two") "— silently, eyes unblinking"))) +(define txs-short + '((span "Not much here!"))) + +(first-words txs-decimals 5) +(first-words txs-punc-and-split-elems 5) +(first-words txs-dashes 5) +(first-words txs-parens-commas 5) +(first-words txs-short 5) +] + +@defproc[(normalize [str string?]) string?]{ + +Removes all non-space/non-alphanumeric characters from @racket[_str], converts it to lowercase, and +replaces all spaces with hyphens. + +@examples[#:eval dust-eval +(normalize "Why, Hello World!") +(normalize "My first-ever 99-argument function, haha") +] + +} + +@section{Article parsers and helpers} + +@defparam[listing-context ctxt (or/c 'blog 'feed 'print "") #:value ""] + +A parameter specifying the current context where any listings of articles would appear. Its purpose +is to allow articles to exclude themselves from certain special collections (e.g., the blog, the RSS +feed, print editions). Any article whose @code{conceal} meta matches the current context will not be +included in any listings returned by the listing functions in +@seclink["cache-rkt"]{@filepath{cache.rkt}}. + +@defproc[(default-title [body-txprs (listof txexpr?)]) string?] + +Given a list of tagged X-expressions (the elements of an article’s doc, e.g.), returns a string +containing a suitable title for the document. (Uses @racket[first-words].) + +Titles are not required for articles, but there are contexts where you need something that serves as +a title if one is not present, and that’s what this function supplies. + +@examples[#:eval dust-eval +(define doc + '(root (p "If I had been astonished at first catching a glimpse of so outlandish an " + "individual as Queequeg circulating among the polite society of a civilized " + "town, that astonishment soon departed upon taking my first daylight " + "stroll through the streets of New Bedford…"))) +(default-title (get-elements doc))] + +@defproc[(current-series-pagenode) pagenode?] + +If @code{(current-metas)} has the key @racket['series], converts its value to the pagenode pointing to +that series, otherwise returns @racket['||]. + +@examples[#:eval dust-eval +(require pollen/core) +(parameterize ([current-metas (hash 'series "marquee-fiction")]) + (current-series-pagenode))] + +@defproc[(current-series-noun) string?] + +If @code{(current-metas)} has the key @racket['series] and if there is a corresponding +@racket[series] in the @racket[series-list], return its @racket[series-noun-singular] value; +otherwise return @racket[""]. + +@defproc[(current-series-title) string?] + +If @code{(current-metas)} has the key @racket['series] and if there is a corresponding +@racket[series] in the @racket[series-list], return its @racket[series-title] value; +otherwise return @racket[""]. + +@defproc[(invalidate-series) (or/c void? boolean?)] + +If the current article specifies a @racket['series] meta, and if a corresponding @filepath{.poly.pm} +file exists in @racket[series-folder], attempts to “touch” the last-modified timestamp on that file, +returning @racket[#t] on success or @racket[#f] on failure. If either precondition is not true, +returns @|void-const|. + +When an article is being rendered, that means the article has changed, and if the article has +changed, its series page (if any) should be updated as well. Touching the @filepath{.poly.pm} file +for a series page triggers a re-render of that page when running @tt{make web} to rebuild the web +content (see @repo-file{makefile}). + +Only used in one place, @repo-file{tags-html.rkt}. + +@defproc[(disposition-values [str string?]) any] + +Given a string @racket[_str], returns two values: the portion of the string coming before the first +space, and the rest of the string. + +@examples[#:eval dust-eval +(disposition-values "* thoroughly recanted")] + +@defproc[(build-note-id [tx txexpr?]) non-empty-string?] + +Given a @code{note} tagged X-expression, returns an identifier string to uniquely identify that note +within an article. This identifier is used as an anchor link in the note’s HTML, and as part of the +note’s primary key in the SQLite cache database. + +@examples[#:eval dust-eval +(build-note-id '(note [[date "2018-02-19"]] "This is an example note")) +(build-note-id '(note [[date "2018-03-19"] [author "Dean"]] "Different author!")) +] + +@defproc[(notes->last-disposition-values [txprs (listof txexpr?)]) any] + +Given a list of tagged X-expressions (ideally a list of @code{note}s), returns two values: the value +of the @racket['disposition] attribute for the last note that contains one, and the ID of that note. + +@examples[#:eval dust-eval +(define notelist + (list + '(note [[date "2018-02-19"] [disposition "* problematic"]] "First note") + '(note [[date "2018-03-19"]] "Second note") + '(note [[date "2018-04-19"] [disposition "† recanted"]] "Third note"))) + +(notes->last-disposition-values notelist)] + +@section{Date formatters} + +@defproc[(ymd->english [ymd-string string?]) string?] + +Converts a date-string of the form @code{"YYYY-MM-DD"} to a string of the form @code{"Monthname D, +YYYY"}. + +If the day number is missing from @racket[_ymd-string], the first day of the month is assumed. If +the month number is also missing, January is asssumed. If the string cannot otherwise be parsed as +a date, an exception is raised. + +If any spaces are present in @racket[_ymd-string], everything after the first space is ignored. + +@defproc[(ymd->dateformat [ymd_string string?] [dateformat string?]) string?] + +Converts a date-string of the form @code{"YYYY-MM-DD"} to another string with the same date +formatted according to @racket[_dateformat]. The +@ext-link["http://unicode.org/reports/tr35/tr35-dates.html#Date_Field_Symbol_Table"]{pattern syntax +of the date format} comes from the Unicode CLDR. ADDED yarn-doc/main.scrbl Index: yarn-doc/main.scrbl ================================================================== --- yarn-doc/main.scrbl +++ yarn-doc/main.scrbl @@ -0,0 +1,58 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt" + racket/runtime-path + (for-label racket/base + "../crystalize.rkt")) + +@title{Local Yarn: Source Code Notes} + +@author{Joel Dueck} + +These are my notes about the internals of the Local Yarn source code. In other words, a personal +reference, rather than a tutorial. + +You’ll get the most out of these notes if you have read @other-doc['(lib +"pollen/scribblings/pollen.scrbl")], and worked through the tutorials there by hand. + +@margin-note{Note that these pages are heavily interlinked with the central Racket documentation at +@tt{docs.racket-lang.org}, which are written and maintained by others. + +Some links from those pages will not work unless you @ext-link["#"]{open this page in its own tab}. +} + +Here’s a rough diagram showing how the @tt{.rkt} modules in this project relate to each other, and +to the Pollen source documents. This is the least complex system I could devise that would @tt{A)} +implement everything I want in my @secref["design-goals"], @tt{B)} cleanly separate dependencies for +print and web output, and @tt{C)} organize an ever-growing collection of hundreds of individual +notes and articles without noticable loss of speed. + +@(define-runtime-path source-diagram "source-diagram.png") +@centered{@responsive-retina-image[source-diagram]} + +The modules are arranged vertically: those on the upper rows provide bindings which are used by +those on the lower rows. The bottom row are the @tt{.poly.pm} files that make up @tech{articles} and +@tech{series}. + +Individual articles, while they are being rendered to HTML pages, save copies of their metadata and +HTML to the SQLite cache. This is done by calling @racket[parse-and-cache-article!] from within +their template. + +Any pages that gather content from multiple articles, such as Series pages and the RSS feed, pull +this content directly from the SQLite cache. This is much faster than trawling through Pollen’s +cached metas of every article looking for matching articles. + +@local-table-of-contents[] + +@include-section["tour.scrbl"] +@include-section["design.scrbl"] +@include-section["pollen.scrbl"] @; pollen.rkt +@include-section["series.scrbl"] +@include-section["dust.scrbl"] @; dust.rkt +@include-section["snippets-html.scrbl"] @; you get the idea +@include-section["cache.scrbl"] +@include-section["crystalize.scrbl"] +@include-section["other-files.scrbl"] ADDED yarn-doc/other-files.scrbl Index: yarn-doc/other-files.scrbl ================================================================== --- yarn-doc/other-files.scrbl +++ yarn-doc/other-files.scrbl @@ -0,0 +1,42 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt") + +@(require (for-label racket/base "../cache.rkt")) + +@title[#:tag "other-files"]{Other files} + +@section{Home page (@filepath{index.html.pp})} + +Simple Pollen preprocessor file that generates the home page. + +@section{Keyword Index (@filepath{keyword-index.rkt})} + +Through its provided @tt{main} function, builds the keyword index page by pulling all the index +entries directly from the SQLite cache and sorting them by first letter. + +@section{Blog (@filepath{blog.rkt})} + +Through its provided @tt{main} function, creates a paginated listing of all @tech{articles} and +@tech{notes}. + +@section{RSS Feed (@filepath{rss-feed.rkt})} + +Through its provided @tt{main} function, creates the RSS feed in the file @filepath{feed.xml}. Both +articles and notes are included. Any article or note with either @racket["all"] or @racket["feed"] +in its @racket['conceal] meta is excluded. + +@section{Cache initialization (@filepath{util/init.rkt})} + +Creates and initializes the cache database with @racket[init-cache-db!] and +@racket[preheat-series!]. + +@section{New article template (@filepath{util/newpost.rkt})} + +Prompts for a title, creates an article with a normalized version of the filename and today’s date, +and opens the article in an editor. + + ADDED yarn-doc/pollen.scrbl Index: yarn-doc/pollen.scrbl ================================================================== --- yarn-doc/pollen.scrbl +++ yarn-doc/pollen.scrbl @@ -0,0 +1,382 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt") +@(require (for-label "../pollen.rkt" + "../dust.rkt" + "../cache.rkt" + "../crystalize.rkt" + racket/base + racket/contract + racket/string + txexpr + pollen/tag + pollen/setup + pollen/core + sugar/coerce)) + +@title[#:tag "pollen-rkt"]{Pollen} + +@defmodule["pollen.rkt" #:packages ()] + +The file @filepath{pollen.rkt} is implicitly @code{require}d in every template and every @code{#lang +pollen} file in the project. It defines the markup for all Pollen documents, and also re-provides +everything provided by @seclink["cache-rkt"]{@filepath{cache.rkt}} and +@seclink["crystalize-rkt"]{@filepath{crystalize.rkt}}. + +The @code{setup} module towards the top of the file is used as described in +@racketmodname[pollen/setup]. + +@section{Markup reference} + +These are the tags that can be used in any of @italic{The Local Yarn}’s Pollen documents (articles, +etc). + +@defproc[(title [element xexpr?] ...) txexpr?]{ + +Supplies a title for the document. You can use any otherwise-valid markup within the title tag. + +Titles are optional; if you don’t specify a title, the article will appear without one. This is +a feature! +} + +@deftogether[(@defproc[(excerpt [elements xexpr?] ...) txexpr?] + @defproc[(excerpt* [elements xexpr?] ...) txexpr?])]{ + +Specify an excerpt to be used when the article or note included in an excerpt-style listing (such as +the blog). The contents of @racket[excerpt] will be extracted out of the article and note and only +appear in listings; if @racket[excerpt*] is used, its contents will be left in place in the +article/note and @emph{reused} as the excerpt in listings. + +} + +@defproc[(p [element xexpr?] ...) txexpr?]{ + +Wrap text in a paragraph. You almost never need to use this tag explicitly; +just separate paragraphs by an empty line. + +Single newlines within a paragraph will be replaced by spaces, allowing you to use +@ext-link["https://scott.mn/2014/02/21/semantic_linewrapping/"]{semantic line wrapping}. +} + +@defproc[(newthought [element xexpr?] ...) txexpr?]{ + +An inline style intended for the first few words of the first paragraph in a new section. Applies +a “small caps” style to the text. Any paragraph containing a @code{newthought} tag is given extra +vertical leading. + +Rule of thumb: within an article, use either @code{section}/@code{subsection} or @code{newthought} +to separate sections of text, but not both. Even better, keep it consistent across articles within +a series. + +If you just need small caps without affecting the paragraph, use @racket[caps]. +} + +@deftogether[(@defproc[(section [element xexpr?] ...) txexpr?] + @defproc[(subsection [element xexpr?] ...) txexpr?])]{ + +Create second- and third-level headings, respectively. This is counting the article's title as the +first-level header (even if the current article has no title). + +} + +@defproc[(block [element xexpr?] ...) txexpr?]{ + +A container for content that should appear grouped together on larger displays. Intended for use in +Series pages, where the template is very minimal to allow for more customization. You would want +output from @racket[(fenced-listing (articles 'short))] to appear inside a @racket[block], but when +using @racket['excerpt] or @racket['full] in place of @racket['short] in that code, you would want +the output to appear outside it (since the “full” and “excerpt” versions of each article effectively +supply their own blocks). Only relevant to HTML output. + +} + +@deftogether[(@defproc[(link [link-id stringish?] [link-text xexpr?]) txexpr?] + @defproc[(url [link-id stringish?] [url string?]) void?])]{ + +All hyperlinks are specified reference-style. So, to link some text, use the @code{link} tag with +an identifier, which can be a string, symbol or number. Elsewhere in the text, use @code{url} with +the same identifier to specify the URL: + +@codeblock|{ + #lang pollen + If you need help, ◊link[1]{Google it}. + + ◊url[1]{https://google.com} +}| + +The @code{url} tag for a given identifier may be placed anywhere in the document, even before it is +referenced. If you create a @code{link} for an identifier that has no corresponding @code{url}, +a @code{"Missing reference: [link-id]"} message will be substituted for the URL. Conversely, +creating a @code{url} that is never referenced will produce no output and no warnings or errors. + +} + +@defproc*[([(xref [title string?]) txexpr?] + [(xref [article-base string?] [element xexpr?] ...) txexpr?])]{ + +Hyperlink to another article within @italic{The Local Yarn} using an @racket[_article-base], which +is the base filename only of an @tech{article} within @racket[articles-folder] (without the +@filepath{.poly.pm} extension). + +If a single argument is supplied (@racket[_title]) it is typeset italicized as the link text, and +its @racket[normalize]d form is used as the article base to generate the link. If more than one +argument is supplied, the first is used as the article base, and the rest are used as the contents +of the link. + +@codeblock|{ + #lang pollen + + A link to ◊xref{My Ultimate Article} will link to “my-ultimate-article.poly.pm”. + A link using ◊xref["my-ultimate-article"]{this form} goes to the same place. +}| + +} + +@deftogether[(@defproc[(figure [image-file string?] [caption xexpr?] ...) txexpr?] + @defproc[(figure-@2x [image-file string?] [caption xexpr?] ...) txexpr?])]{ + +Insert a block-level image. The @racket[_image-file] should be supplied as a filename only, with no +folder names. It is assumed that the image is located inside an @racket[images-folder] within the +same folder as the source document. + +For web output, using @racket[figure-@2x] will produce an image hard-coded to display at half its +actual size, or the width of the text block, whichever is smaller. + +} + +@defproc[(image-link [image-file string?] [link-text xexpr?] ...) txexpr?]{ + +Adds a hyperlink to @racket[_image-file], supplied as a filename only with no folder names. It is +assumed that the image is located inside an @racket[images-folder] within the same folder as the +source document. + +} + +@deftogether[(@defproc[(fn [fn-id stringish?]) txexpr?] + @defproc[(fndef [fn-id stringish?] [elements xexpr?] ...) txexpr?])]{ + +As with hyperlinks, footnotes are specified reference-style. In the output, footnotes will be +numbered according to the order in which their identifiers are referenced in the source document. + +Example: + +@codeblock|{ + #lang pollen + Shoeless Joe Jackson was one of the best players of all time◊fn[1]. + + ◊fndef[1]{But he might have lost the 1919 World Series on purpose.} +}| + +You can refer to a given footnote definition more than once. + +The @code{fndef} for a given id may be placed anywhere in the source document, even before it is +referenced. If you create a @code{fn} reference without a corresponding @code{fndef}, +a @code{"Missing footnote definition!"} message will be substituted for the footnote text. +Conversely, creating a @code{fndef} that is never referenced will produce no output, warning or +error. + +} + +@deftogether[(@defproc[(dialogue [elements xexpr?] ...) txexpr?] + @defproc[(say [interlocutor string?] [elements xexpr?] ...) txexpr?] + @defproc[(saylines [interlocutor string?] [elements xexpr?] ...) txexpr?])]{ + +Use these tags together for transcripts of dialogue, chats, screenplays, interviews and so +forth. The @racket[saylines] tag is the same as @racket[say] except that within @racket[saylines], +linebreaks within paragraphs are preserved. + +Example usage: + +@codeblock|{ + #lang pollen + + ◊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 [#:key key string? ""] [elements xexpr?] ...) txexpr?]{ + +Creates a bidirectional link between this spot in the document and an entry in the keyword index +under @racket[_key]. If @racket[_key] is not supplied, the string contents of @racket[_elements] are +used as the key. Use @tt{!} to split @racket[_key] into a main entry and a subentry. + +The example below will create two index entries, one under the heading “compassion” and one under +the main heading "cats" and a subheading “stray”: + +@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}. That if we lavish our concern on every + stray ◊index[#:key "cats!stray"]{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?]{ + +Add a @tech{note} to the “Further Notes” section of the article. + +The @code{#:date} attribute is required and must be of the form @tt{YYYY-MM-DD}. + +The @code{#:author} and @code{#:author-url} attributes can be used to credit notes from other +people. If the @code{#:author} attribute is not supplied then the value of @code{default-authorname} +is used. + +The @code{#:disposition} attribute is used for notes that update or alter the whole disposition of +the article. It must be a string of the form @racket[_mark _past-tense-verb], where @racket[_mark] +is a symbol suitable for use as a marker, such as * or †, and @racket[_past-tense-verb] is the word +you want used to describe the article’s current state. An article stating a metaphysical position +might later be marked “recanted”; a prophecy or prediction might be marked “fulfilled”. + +@codeblock|{ +#lang pollen + +◊note[#:date "2019-02-19" #:disposition "✓ verified"]{I wasn’t sure, but now I am.} +}| + + +If more than one note contains a @code{disposition} attribute, the one from the most recent note is +the one used. + +Some caveats (for now): + +@itemlist[ + @item{Avoid defining new footnotes using @code{fndef} inside a @code{note}; these footnotes will + be placed into the main footnote section of the article, which is probably not what you want.} +] + +} + +@defproc[(verse [#:title title string? ""] [#:italic? italic boolean? #f] [element xexpr?] ...) + txexpr?]{ + +Typeset contents as poetry, with line breaks preserved and the block centered on the longest line. +To set the whole block in italic, use @code{#:italic? #t} — otherwise, use @code{i} within the +block. + +If the first element in an article is a @racket[verse] tag with the @racket[#:title] attribute +specified, that title is used as the article’s title if the normal @racket[title] tag is absent. + +To cite a source, use @racket[attrib] immediately afterward. + +} + +@defproc[(magick [element xexpr?] ...) txexpr?]{ + +Typeset contents using historical ligatures and the “long s” conventions of 17th-century English +books. + +} + +@defproc[(blockquote [element xexpr?] ...) txexpr?]{ + +Surrounds a block quotation. To cite a source, use @racket[attrib] immediately afterward. + +} + +@defproc[(attrib [element xexpr?] ...) txexpr?]{ + +An attribution line, for citing a source for a block quotation, epigraph or poem. + +} + +@defproc[(blockcode [element xexpr?] ...) txexpr?]{ + +Typeset contents as a block of code using a monospace font. Line breaks are preserved. + +} + +@deftogether[(@defproc[(i [element xexpr?] ...) txexpr?] + @defproc[(em [element xexpr?] ...) txexpr?] + @defproc[(b [element xexpr?] ...) txexpr?] + @defproc[(mono [element xexpr?] ...) txexpr?] + @defproc[(strong [element xexpr?] ...) txexpr?] + @defproc[(strike [element xexpr?] ...) txexpr?] + @defproc[(ol [element xexpr?] ...) txexpr?] + @defproc[(ul [element xexpr?] ...) txexpr?] + @defproc[(item [element xexpr?] ...) txexpr?] + @defproc[(sup [element xexpr?] ...) txexpr?] + @defproc[(caps [element xexpr?] ...) txexpr?] + @defproc[(code [element xexpr?] ...) txexpr?])]{ + +Work pretty much how you’d expect. + +} + +@section{Convenience macros} + +@defform[(for/s thing-id listofthings result-exprs ...) + #:contracts ([listofthings (listof any/c)])]{ + +A shorthand form for Pollen’s @code{for/splice} that uses far fewer brackets when you’re only +iterating through a single list. + +@codeblock|{ +#lang pollen + +◊for/s[x '(7 8 9)]{Now once for number ◊x} + +◊;Above line is shorthand for this one: +◊for/splice[[(x (in-list '(7 8 9)))]]{Now once for number ◊x} +}| + +} + +@section{Defining new tags} + +I use a couple of macros to define tag functions that automatically branch into other functions +depending on the current output target format. This allows me to put the format-specific tag +functions in separate files that have separate places in the dependency chain. So if only the HTML +tag functions have changed and not those for PDF, the @filepath{makefile} can ensure only the HTML +files are rebuilt. + +@defproc[#:kind "syntax" + (poly-branch-tag (tag-id symbol?)) + (-> txexpr?)]{ + +Defines a new function @racket[_tag-id] which will automatically pass all of its arguments to a +function whose name is the value returned by @racket[current-poly-target], followed by a hyphen, +followed by @racket[_tag]. So whenever the current output format is @racket['html], the function +defined by @racket[(poly-branch-tag _p)] will branch to a function named @racket[html-p]; when the +current format is @racket['pdf], it will branch to @racket[pdf-p], and so forth. + +You @emph{must} define these branch functions separately, and you must define one for @emph{every} +output format included in the definition of @racket[poly-targets] in this file’s @racket[setup] +submodule. If you do not, you will get “unbound identifier” errors at expansion time. + +The convention in this project is to define and provide these branch functions in separate files: +see, e.g., @filepath{tags-html.rkt}. + +Functions defined with this macro @emph{do not} accept keyword arguments. If you need keyword +arguments, see @racket[poly-branch-kwargs-tag]. + +@margin-note{The thought behind having two macros so similar is that, by cutting out handling for keyword +arguments, @racket[poly-branch-tag] could produce simpler and faster code. I have not verified if +this intuition is meaningful or correct.} + +} + +@defproc[#:kind "syntax" + (poly-branch-kwargs-tag (tag-id symbol?)) + (-> txexpr?)]{ + +Works just like @racket[poly-branch-tag], but uses Pollen’s @racket[define-tag-function] so that +keyword arguments will automatically be parsed as X-expression attributes. + +Additionally, the branch functions called from the new function must accept exactly two arguments: +a list of attributes and a list of elements. + +} ADDED yarn-doc/scribble-helpers.rkt Index: yarn-doc/scribble-helpers.rkt ================================================================== --- yarn-doc/scribble-helpers.rkt +++ yarn-doc/scribble-helpers.rkt @@ -0,0 +1,79 @@ +#lang racket/base + +; SPDX-License-Identifier: BlueOak-1.0.0 +; This file is licensed under the Blue Oak Model License 1.0.0. + +;; Convenience/helper functions for this project’s Scribble documentation + +(require scribble/core + scribble/manual/lang + scribble/html-properties + scribble/private/manual-sprop + scribble/decode + racket/runtime-path + (only-in net/uri-codec uri-encode)) +(provide (all-defined-out)) + +(define-runtime-path custom-css "custom.css") + +(define repo-url/ "https://thelocalyarn.com/code/") + +;; Link to a ticket on the Fossil repository by specifying the ticket ID. +;; The "_parent" target breaks out of the iframe used by the Fossil repo web UI. +(define (ticket id-str) + (hyperlink (string-append repo-url/ "tktview?name=" id-str) + "ticket " + (tt id-str) + #:style (style #f (list (attributes '((target . "_parent"))))))) + +;; Link to a wiki page on the Fossil repository by specifying the title +(define (wiki title) + (hyperlink (string-append repo-url/ "wiki?name=" (uri-encode title)) + title + #:style (style #f (list (attributes '((target . "_parent"))))))) + +;; Link somewhere outside these docs or Racket docs. The `_blank` target opens in a new tab. +(define (ext-link url-str . elems) + (keyword-apply hyperlink '(#:style) (list (style #f (list (attributes '((target . "_blank")))))) + url-str + elems)) + +;; Link to show contents of the latest checked-in version of a file +;; (or a file listing if a directory was specified) +(define (repo-file filename) + (hyperlink (string-append repo-url/ "file/" filename) + (tt filename) + #:style (style #f (list (attributes '((target . "_parent"))))))) + +(define (responsive-retina-image img-path) + (image img-path + #:scale 0.5 + #:style (style #f (list (attributes '((style . "max-width:100%;height:auto;"))))))) + +;; +;; From https://github.com/mbutterick/pollen/blob/master/pollen/scribblings/mb-tools.rkt +;; + +(define (terminal . args) + (compound-paragraph (style "terminal" (list (css-style-addition custom-css) (alt-tag "div"))) + (list (apply verbatim args)))) + +(define (cmd . args) + (elem #:style (style #f (list (color-property "black"))) (tt args))) + +(define (fileblock filename . inside) + (compound-paragraph + (style "fileblock" (list* (alt-tag "div") 'multicommand + (box-mode "RfileboxBoxT" "RfileboxBoxC" "RfileboxBoxB") + scheme-properties)) + (list + (paragraph (style "fileblock_filetitle" (list* (alt-tag "div") (box-mode* "RfiletitleBox") scheme-properties)) + (list (make-element + (style "fileblock_filename" (list (css-style-addition custom-css))) + (if (string? filename) + (filepath filename) + filename)))) + (compound-paragraph + (style "fileblock_filecontent" (list* (alt-tag "div") (box-mode* "RfilecontentBox") scheme-properties)) + (decode-flow inside))))) + ADDED yarn-doc/series.scrbl Index: yarn-doc/series.scrbl ================================================================== --- yarn-doc/series.scrbl +++ yarn-doc/series.scrbl @@ -0,0 +1,74 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt" + scribble/example) + +@(require (for-label "../pollen.rkt" + "../series-list.rkt" + "../dust.rkt" + "../cache.rkt" + pollen/core + racket/base + racket/contract)) + +@title{Defining Series} + +To create a new series: + +@itemlist[#:style 'ordered + + @item{Create a file @filepath{my-key.poly.pm} inside @racket[series-folder] and include a call + to @racket[fenced-listing] to list all the articles and notes that will be included in the series: + @codeblock|{ +#lang pollen + +◊(define-meta title "My New Series") + +◊block{Here’s what we call a bunch of similar articles: + +◊(fenced-listing (articles 'short)) + +} + }| + } + + @item{Add an entry for @racket[_my-key] to @racket[series-list] in @filepath{series-list.rkt}} + + @item{Use @racket[(define-meta series "my-key")] in articles to add them to the series.} + + @item{If @racket[series-ptree-ordered?] is @racket[#t], create a @seclink["Pagetree" #:doc '(lib + "pollen/scribblings/pollen.scrbl")]{pagetree} file in @racket[series-folder] named + @filepath{my-key.ptree}.} + + ] + +@section{Series list} + +@defmodule["series-list.rkt" #:packages ()] + +This module contains the most commonly used bits of meta-info about @tech{series}. Storing these +bits in a hash table of structs makes them faster to retrieve than when they are stored inside the +metas of the Pollen documents for the series themselves. + +@defthing[series-list hash?]{ + +An immutable hash containing all the title and noun info for each @tech{series}. Each key is +a string and each value is a @racket[series] struct. + +} + +@defstruct[series ([key string?] + [title string?] + [noun-plural string?] + [noun-singular string?] + [ptree-ordered? boolean?])]{ + +Struct for holding metadata for a @tech{series}. The @racket[_ptree-ordered?] value should be +@racket[#t] if there is a @filepath{@italic{key}.ptree} file in @racket[series-folder] that provides +information on how articles in the series are ordered. + +} + ADDED yarn-doc/snippets-html.scrbl Index: yarn-doc/snippets-html.scrbl ================================================================== --- yarn-doc/snippets-html.scrbl +++ yarn-doc/snippets-html.scrbl @@ -0,0 +1,161 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt") + +@(require (for-label "../pollen.rkt" + "../dust.rkt" + "../snippets-html.rkt" + racket/base + racket/contract + racket/string + pollen/template + pollen/pagetree + txexpr + sugar/coerce)) + +@title{HTML snippets} + +@defmodule["snippets-html.rkt" #:packages ()] + +Each “snippet” module provides all (well @emph{most of}) the document- and article-level blocks of +structural markup necessary for a particular target output format; this one is for HTML. The idea is +that any block of markup that might be reused across more than one template should be a function. + +The functions in the snippets modules follow two conventions in this project: + +@itemlist[ + @item{Functions that return strings of HTML have the prefix @tt{html$-}.} + @item{Such functions do not do any parsing or destructuring of complex objects; every separate + piece that will be inserted into the template is passed in as a separate argument. This makes it + harder to change the scope of what a snippet does, but makes things faster since all the parsing + can happen in one place, before the snippet functions are called.} ] + +@section{Using @tt{pollen/mode}} + +It’s worth looking at the source for this file to see how @racketmodname[pollen/mode] can be used to +make it easy to write “mini-template” functions: + +@codeblock{ +#lang pollen/mode racket/base + +(define (html$-my-article title body-tx) + ◊string-append{ + <p><b>◊|title|</b><p> + ◊(->html body-tx) + }) +} + +@section{HTML Snippet functions} + +@defproc[(html$-page-head [title (or/c string? #f) #f] [close-head? boolean? #t]) +non-empty-string?]{ + +Returns the @tt{<head>} section of an HTML document. + +If @racket[_title] is a string it will be used inside the @tt{<title>} tag. + +If you want to include additional stuff inside the @tt{<head>}, you can set @racket[_close-head?] to +@racket[#f] to prevent it from including the closing @tt{</head>} tag (you’ll have to add it +yourself). +} + +@defproc[(html$-page-body-open [body-class string? ""]) non-empty-string?]{ + +Returns the opening @tt{<body>} and @tt{<main>} tags and elements that immediately follow, such as +site header, logo and navigation. + +If @racket[_body-class] is a non-empty string, its contents will be included in the @tt{class} +attribute of the @tt{<body>} tag. +} + +@defproc[(html$-series-list) non-empty-string?]{ + +Returns an HTML @tt{<section>} containing a list of all series, grouped by their “plural nouns”. The +grouped list will flow into columns on wider displays. +} + +@defproc[(html$-article-open [pagenode pagenode?] + [title-specified-in-doc? boolean?] + [title txexpr?] + [pubdate string?]) + non-empty-string?]{ + +Returns the opening @tt{<article>} tag and elements that immediately follow: permlink, publish date, +and opening @tt{<section>} tag. + +The @racket[_title-specified-in-doc?] form changes the HTML markup structure used. +} + +@defproc[(html$-article-close [footertext string?]) non-empty-string?]{ + +Returns a string containing a closing @tt{<section>} tag, a @tt{<footer>} element containing +@racket[_footertext], and a closing @tt{<article>} tag. If @racket[_footertext] is empty, the +@tt{<footer>} element will be omitted. +} + +@defproc[(html$-article-listing-short [pagenode pagenode?] [pubdate string?] [title string?]) +non-empty-string?]{ + +Returns a string of HTML for an article as it would appear in a listing context in “short” form +(date and title only, with link). +} + +@defproc[(html$-page-footer) non-empty-string?]{ +Returns an HTML @tt{<footer>} tag for use at the bottom of each page. +} + +@defproc[(html$-page-body-close) non-empty-string?]{ + +Returns a string containing the page’s @tt{<footer>} and closing tags. +} + +@defproc[(html$-note-contents [disposition-mark string?] [elems (listof xexpr?)]) +non-empty-string?]{ + +Returns a string containing the body-elements of a note converted to HTML. If +@racket[_disposition-mark] is not empty, a @tt{<span>} containing it will be inserted as the first +element of the first block-level element. +} + +@defproc[(html$-note-listing-full [pagenode pagenode?] + [note-id string?] + [title-html-flow string?] + [date string?] + [contents string?] + [author string? (default-authorname)] + [author-url string? ""]) + non-empty-string?]{ + +Returns a string containing the complete HTML markup for a @racket[note], including title, permlink, +date, contents and author, suitable for display outside the context of its parent article. +} + +@defproc[(html$-note-in-article [id string?] + [date string?] + [contents string?] + [author string?] + [author-url string?]) non-empty-string?]{ + +Like @racket[html$-note-listing-full], but returns HTML for a @racket[note] suitable for display +inside its parent article. +} + +@defproc[(html$-notes-section [note-htmls string?]) non-empty-string?]{ + +Returns the complete HTML for the @italic{Further Notes} section of an article. +} + +@defproc[(html$-paginate-navlinks [current-page exact-positive-integer?] + [pagecount exact-positive-integer?] + [basename string?]) string?]{ + +On the “blog”, the articles are split across multiple files: @filepath{blog-pg1.html}, +@filepath{blog-pg2.html}, etc. This function provides a string containing HTML for a group of links +that can be given within each file, to link to the pages that come before/after it. + +The links are enclosed within @tt{<li>} tags. It’s up to the calling site to provide the enclosing +@tt{<ul>} tag (in case any custom styling or IDs need to be applied). +} ADDED yarn-doc/tour.scrbl Index: yarn-doc/tour.scrbl ================================================================== --- yarn-doc/tour.scrbl +++ yarn-doc/tour.scrbl @@ -0,0 +1,137 @@ +#lang scribble/manual + +@; SPDX-License-Identifier: BlueOak-1.0.0 +@; This file is licensed under the Blue Oak Model License 1.0.0. + +@(require "scribble-helpers.rkt") + +@(require (for-label racket/base pollen/core "../pollen.rkt")) + +@title{How I Publish: A Quick Tour} + +This isn’t a tutorial, since these steps probably won’t all work on your computer. Think of these +narrations like me talking while I drive. + +@section{Creating an article} + +Open a terminal window. + +@terminal{@cmd{> cd /path/to/thelocalyarn}} + +The @tt{make} command provides a high-level control panel for common tasks. Typing just make from +a terminal window shows a list of options: + +@terminal{ +@cmd{> make} +article Start a new article from a template +help Displays this help screen +publish Sync all HTML and PDF stuff to the public web server +scribble Rebuild code documentation and update Fossil repo +web Rebuild all web content (not PDFs) +zap Clear Pollen and Scribble cache, and remove all HTML output +} + +Following the first option in this list, I type @tt{make article}, and enter a post title when +prompted: + +@terminal{ +@cmd{> make article} +racket -tm util/newpost.rkt +Enter title: @cmd{My New Post} +} + +The script creates a new @filepath{.poly.pm} file using a normalized version of the title for the +filename, and opens it in my editor (this is currently hardcoded to use MacVim). When the file pops +up we see a basic template ready to edit: + +@filebox["articles/my-new-post.poly.pm" +@codeblock|{ +#lang pollen + +◊; Copyright 2020 by Joel Dueck. All Rights Reserved. +◊(define-meta conceal "blog,rss") +◊(define-meta published "2020-01-18") + +◊title{My New Post} + +Write here! +}|] + +At this point I might delete the @tt{◊title} line, since specifying a formal title is optional +(other than the one needed to generate the filename). I might also add a @racket[define-meta] for +@tt{series} or @tt{topics}. + +As long as the @racket[define-meta] for @tt{conceal} contains @racket{rss}, the new article will not +appear in the RSS feed; as long as it contains @racket{blog} it will not appear in the blog. This is +useful for when an article is in a draft state, or simply when you want to keep it semi-hidden. + +When satisfied with the post I’ll remove the @racket[define-meta] for @tt{conceal}, save it one last +time, then go back to the terminal: + +@terminal{ +@cmd{> make web} +[lots of output: rebuilds blog pages, keyword index, RSS feed] +@cmd{> make publish} +[lots of output: uploads all web content to the server] +} + +The article also needs to be added to the Fossil repository, so revisions to it will be tracked: + +@terminal|{ +|@cmd{> fossil add article/my-new-post.poly.pm} +ADDED article/my-new-post.poly.pm + +|@cmd{> fossil commit -m "Publish ‘My New Post’"} +Autosync: https://joel@thelocalyarn.com/code +Round-trips: 2 Artifacts sent: 0 received: 1 +Pull done, sent: 892 received: 8720 ip: 162.243.186.132 +New_Version: 15507b62416716a3f0be3c444b0fc09aa4364b989140c5788cf679eb0b2463a6 +Autosync: https://joel@thelocalyarn.com/code +Round-trips: 2 Artifacts sent: 2 received: 1 +Sync done, sent: 10153 received: 4680 ip: 162.243.186.132 +}| + +As you can see, Fossil does an automatic pull before the commit, and another automatic push +afterwards. This commit is now visible on the public timeline, and the source code for the article +can now be seen on the public repo at @tt{thelocalyarn.com/code/}. + +@section{Adding notes to an article} + +A few days (or years) after doing the above, I receive an email from Marjorie with commenting on +@italic{My New Post} and I decide to publish her comments. + +I open the article in my editor and add some lines to the end: + +@filebox["articles/my-new-post.poly.pm" +@codeblock|{ +#lang pollen + +◊; Copyright 2020 by Joel Dueck. All Rights Reserved. +◊(define-meta published "2020-01-18") + +◊title{My New Post} + +It’s a 4⨉4 treated. I got twenty, actually, for the backyard fence. + +◊note[#:date "2020-01-23" #:author "Marjorie"]{ + Hope you sank that thing below the frost line. +} +}|] + +This looks like a blog-style comment, but the @racket[note] tag function has some special powers +that typical comments don’t have, as we’ll see in a moment. + +I save this, go back to the terminal and do @tt{make web} and @tt{make publish} as before. + +Now if you open the article’s permlink, you’ll see the note appears in a “Further Notes” section at +the bottom — again, just like a normal blog post comment. + +But if you go to the Blog section, you’ll see the note appearing in its own space right alongside +the other articles, as if it were a separate post. It will also appear in a separate entry in the +RSS feed. + +@section{What’s not here yet} + +Eventually there will be facilities for creating PDF files of individual articles, and print-ready +PDFs of books containing collections of articles. + ADDED yarn-lib/markup.rkt Index: yarn-lib/markup.rkt ================================================================== --- yarn-lib/markup.rkt +++ yarn-lib/markup.rkt @@ -0,0 +1,313 @@ +#lang racket/base + +; SPDX-License-Identifier: BlueOak-1.0.0 +; This file is licensed under the Blue Oak Model License 1.0.0. + +;; Tag functions used by pollen.rkt when HTML is the output format. + +(require (for-syntax racket/base racket/syntax)) +(require racket/list + racket/function + racket/draw + racket/class + pollen/decode + pollen/tag + pollen/setup + pollen/core + net/uri-codec + txexpr + "dust.rkt") + +(provide html-fn + html-fndef) + +;; Customized paragraph decoder replaces single newlines within paragraphs +;; with single spaces instead of <br> tags. Allows for “semantic line wrapping”. +(define (decode-hardwrapped-paragraphs xs) + (define (no-linebreaks xs) + (decode-linebreaks xs " ")) + (decode-paragraphs xs #:linebreak-proc no-linebreaks)) + +;; A shortcut macro: lets me define a whole lot of tag functions of the form: +;; (define html-p (default-tag-function 'p) +(define-syntax (provide/define-html-default-tags stx) + (syntax-case stx () + [(_ TAG ...) + (let ([tags (syntax->list #'(TAG ...))]) + (with-syntax ([((HTML-TAG-FUNC HTML-TAG) ...) + (for/list ([htag (in-list tags)]) + (list (format-id stx "html-~a" (syntax-e htag)) (syntax-e htag)))]) + #'(begin + (provide HTML-TAG-FUNC ...) + (define HTML-TAG-FUNC (default-tag-function 'HTML-TAG)) ...)))])) + +;; Here we go: +(provide/define-html-default-tags p + b + strong + i + em + ol + ul + sup + blockquote + code) + +(provide html-root + html-title + html-excerpt + html-excerpt* + html-item + html-section + html-subsection + html-newthought + html-sep + html-caps + html-mono + html-center + html-strike + html-block + html-blockcode + html-index + html-figure + html-figure-@2x + html-image-link + html-dialogue + html-say + html-saylines + html-magick + html-verse + html-attrib + html-link + html-xref + html-url + html-fn + html-fndef + html-note-with-srcline) + +(define html-item (default-tag-function 'li)) +(define html-section (default-tag-function 'h2)) +(define html-subsection (default-tag-function 'h3)) +(define html-newthought (default-tag-function 'span #:class "newthought")) +(define (html-sep) '(hr [[class "sep"]])) +(define html-caps (default-tag-function 'span #:class "caps")) +(define html-center (default-tag-function 'div #:style "text-align: center")) +(define html-strike (default-tag-function 'span #:style "text-decoration: line-through")) +(define html-dialogue (default-tag-function 'dl #:class "dialogue")) +(define html-mono (default-tag-function 'samp)) + +(define (html-block . elements) + `(section [[class "content-block"]] (div [[class "content-block-main"]] ,@elements))) + +(define (html-root . elements) + (invalidate-series) + (define first-pass + (decode-elements (append elements (list (html-footnote-block))) + #:txexpr-elements-proc decode-hardwrapped-paragraphs + #:exclude-tags '(script style figure table pre))) + (define second-pass + (decode-elements first-pass + #:block-txexpr-proc detect-newthoughts + #:inline-txexpr-proc decode-link-urls + #:exclude-tags '(script style pre code))) + `(body ,@second-pass)) + +(define (html-title . elements) `(title ,@elements)) +(define (html-excerpt . elements) `(excerpt ,@elements)) +(define (html-excerpt* . elements) `(excerpt* ,@elements)) + +(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 attrs elems) + (define index-key (maybe-attr 'key attrs (tx-strs `(span ,@elems)))) + `(a [[id ,(here-id (list "_idx-" (uri-encode index-key)))] + [href ,(string-append "/keyword-index.html#" (uri-encode (string-downcase index-key)))] + [data-index-entry ,index-key] + [class "index-link"]] + ,@elems)) + +;; To be used within ◊dialogue +(define (html-say . elems) + `(@ (dt ,(car elems) (span [[class "x"]] ": ")) (dd ,@(cdr elems)))) + +;; Same as ◊say, but preserve linebreaks +(define (html-saylines . elems) + (apply html-say (decode-linebreaks elems))) + +(define (html-verse attrs elems) + (let* ([title (maybe-attr 'title attrs "")] + [italic? (assoc 'italic? attrs)] + [pre-attrs (cond [italic? '([class "verse"] [style "font-style: italic"])] + [else '([class "verse"])])] + [pre-title (cond [(string>? title "") `(p [[class "verse-heading"]] ,title)] + [else ""])]) + `(div [[class "poem"]] ,pre-title (pre ,pre-attrs ,@elems)))) + +(define (html-magick . elems) + (txexpr + 'div '([class "antique"]) + (decode-elements + elems + #:string-proc + (λ (s) (regexp-replace* #px"(?<!f)s(?![fkb\\s”,;.’:\\!\\?]|$)" s "ſ"))))) + +(module+ test + (require rackunit) + ; always round s at the end of a word + (check-equal? (html-magick "mirrors? yes, it is") '(div [[class "antique"]] "mirrors? yes, it is")) + ; always round s before/after f + (check-equal? (html-magick "offset, satisfaction") '(div [[class "antique"]] "offset, ſatisfaction")) + ; always LONG s before hyphen + (check-equal? (html-magick "Shafts-bury") '(div [[class "antique"]] "Shaftſ-bury")) + ; always round s before k or b (17th-century rules) + (check-equal? (html-magick "ask, husband") '(div [[class "antique"]] "ask, husband")) + ; always LONG s everywhere else + (check-equal? (html-magick "song, substitutes") '(div [[class "antique"]] "ſong, ſubſtitutes")) + + ;; Nested elements + (check-equal? + (html-magick '(root "This is " (a [[href "class"]] (b "song, substitutes")))) + '(div [[class "antique"]] (root "This is " (a [[href "class"]] (b "ſong, ſubſtitutes")))))) + +(define html-attrib (default-tag-function 'div #:class "attrib")) + +;; (Private) Get the dimensions of an image file +(define (get-image-size filepath) + (define bmp (make-object bitmap% filepath)) + (list (send bmp get-width) (send bmp get-height))) + +;; (Private) Builds a path to an image in the [image-dir] subfolder of the current document's +;; folder, relative to the current document’s folder +(define (image-source basename) + (define here-path (string->path (maybe-meta 'here-path))) + (define-values (_ here-rel-path-parts) + (drop-common-prefix (explode-path (current-project-root)) + (explode-path here-path))) + (let* ([folder-parts (drop-right here-rel-path-parts 1)] + [img-path-parts (append folder-parts (list images-folder basename))] + [img-path (apply build-path/convention-type 'unix img-path-parts)]) + (path->string img-path))) + +(define (html-figure-@2x . elems) + (define src (image-source (car elems))) + (define alt-text (tx-strs `(span ,@(cdr elems)))) + (define img-width (car (get-image-size (build-path (current-project-root) src)))) + (define style-str (format "width: ~apx" (/ img-width 2.0))) + `(figure (img [[alt ,alt-text] [style ,style-str] [src ,(string-append web-root src)]]) + (figcaption ,@(cdr elems)))) + +(define (html-figure . elems) + (define src (string-append web-root (image-source (car elems)))) + (define alt-text (tx-strs `(span ,@(cdr elems)))) + `(figure [[class "fullwidth"]] + (img [[alt ,alt-text] [src ,src]]) + (figcaption ,@(cdr elems)))) + +;; Simple link to an image +(define (html-image-link . elems) + (define src (image-source (car elems))) + (define title (tx-strs `(span ,@(cdr elems)))) + `(a [[href ,(string-append web-root src)] [title ,title]] ,@(cdr elems))) + +;; There is no way in vanilla CSS to create a selector for “p tags that contain +;; a span of class ‘newthought’”. So we can handle it at the Pollen processing level. +(define (detect-newthoughts block-xpr) + (define (is-newthought? tx) ; Helper function + (and (txexpr? tx) + (eq? 'span (get-tag tx)) + (attrs-have-key? tx 'class) + (string=? "newthought" (attr-ref tx 'class)))) + (if (and (eq? (get-tag block-xpr) 'p) + (is-newthought? (first (get-elements block-xpr)))) + (attr-set block-xpr 'class "pause-before") + block-xpr)) + +;; Links +;; +;; Private use: +(define all-link-urls (make-hash)) + +;; Provided tag functions: +(define (html-link . args) + `(link& [[ref ,(format "~a" (first args))]] ,@(rest args))) + +(define (html-url ref url) + (define page-path (hash-ref (current-metas) 'here-path)) + (define page-link-urls (hash-ref! all-link-urls page-path make-hash)) + (hash-set! page-link-urls (format "~a" ref) url) "") + +;; Private use (by html-root): +(define (decode-link-urls tx) + (define page-path (hash-ref (current-metas) 'here-path)) + (define page-link-urls (hash-ref! all-link-urls page-path make-hash)) + (cond [(eq? (get-tag tx) 'link&) + (let* ([url-ref (attr-ref tx 'ref)] + [url (or (hash-ref page-link-urls url-ref #f) + (format "Missing reference: ~a" url-ref))]) + `(a [[href ,url]] ,@(get-elements tx)))] + [else tx])) + +;; Fast link to another article +(define html-xref + (case-lambda + [(title) `(a [[href ,(format "~aarticles/~a.html" web-root (normalize title))] + [class "xref"]] + (i ,title))] + [elems `(a [[href ,(format "~aarticles/~a.html" web-root (first elems))] + [class "xref"]] + ,@(rest elems))])) + +;; Footnotes +;; +;; Private use: +(define all-fn-names (make-hash)) +(define all-fn-definitions (make-hash)) +(define (fn-id x) (here-id (string-append x "_fn"))) +(define (fndef-id x) (here-id (string-append x "_fndef"))) + +;; Provided footnote tag functions: +(define (html-fn . args) + (define name (format "~a" (first args))) + (define page-path (hash-ref (current-metas) 'here-path)) + (define page-fn-names (cons name (hash-ref! all-fn-names page-path '()))) + (hash-set! all-fn-names page-path page-fn-names) + + (let* ([def-anchorlink (string-append "#" (fndef-id name))] + [nth-ref (number->string (count (curry string=? name) page-fn-names))] + [ref-id (string-append (fn-id name) nth-ref)] + [fn-number (+ 1 (index-of (remove-duplicates (reverse page-fn-names)) name))] + [ref-text (format "(~a)" fn-number)]) + (cond [(empty? (rest args)) `(sup (a [[href ,def-anchorlink] [id ,ref-id]] ,ref-text))] + [else `(span [[class "links-footnote"] [id ,ref-id]] + ,@(rest args) + (sup (a [[href ,def-anchorlink]] ,ref-text)))]))) + +(define (html-fndef . elems) + (define page-path (hash-ref (current-metas) 'here-path)) + (define page-fn-defs (hash-ref! all-fn-definitions page-path make-hash)) + (hash-set! page-fn-defs (format "~a" (first elems)) (rest elems))) + +;; Private use (by html-root) +(define (html-footnote-block) + (define page-path (hash-ref (current-metas) 'here-path)) + (define page-fn-names (hash-ref! all-fn-names page-path '())) + (define page-fn-defs (hash-ref! all-fn-definitions page-path (make-hash))) + (define note-items + (for/list ([fn-name (in-list (remove-duplicates (reverse page-fn-names)))]) + (let* ([definition-text (or (hash-ref page-fn-defs fn-name #f) + '((i "Missing footnote definition!")))] + [backref-count (count (curry string=? fn-name) page-fn-names)] + [backrefs (for/list ([fnref-num (in-range backref-count)]) + `(a [[href ,(string-append "#" + (fn-id fn-name) + (format "~a" (+ 1 fnref-num)))]] "↩"))]) + `(li [[id ,(fndef-id fn-name)]] ,@definition-text ,@backrefs)))) + (cond [(null? note-items) ""] + [else `(section ((class "footnotes")) (hr) (ol ,@note-items))])) + +(define (html-note-with-srcline attrs elems) + (txexpr 'note attrs (decode-hardwrapped-paragraphs elems)))