#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"
                     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{@filepath{dust.rkt}}

@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.

@defproc[(here-output-path) path?]

Returns the path to the current output file, relative to @racket[current-project-root]. If no metas
are available, returns @racket[(string->path ".")]. This is like what you can get from the @tt{here}
variable that Pollen provides, except it is available outside templates.

@defproc[(here-id [suffix (or/c (listof string?) string? #f) #f]) string?]

Returns the 8-character prefix of the SHA1 hash of the current document’s output path. If no metas
are available, 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 is @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))

(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?"))]

@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)
]

@section{Article parsers and helpers}

@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[(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['||].

@defproc[(series-noun) string?]

If @code{(current-metas)} has the key @racket['series], and if the corresponding series defines a meta
value for @racket['noun-singular], then return it, otherwise return @racket[""].

@defproc[(series-title) string?]

If @code{(current-metas)} has the key @racket['series], and if the corresponding series defines a meta
value for @racket['title], then return it, 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.
