Scribble themes
| (require scribble-theme) | package: scribble-theme |
Scribble documents published as HTML already look great (at least, the ones written in #lang scribble/manual). If you want to thoroughly customize the look of the rendered HTML, though, it can be tricky, so I made this package to make it a bit easier.
In my case, I wanted Scribble docs for my packages to render normally when rendered as part of a local install or the main site https://docs.racket-lang.org, but I wanted to easily substitute my own CSS when publishing to my website.
In order to attempt this, you have to be handy with CSS.
1 How it works
A theme is a CSS file (and optionally additional CSS files it references via @import directives). Theming, for our purposes, involves substituting your own CSS file(s) for the ones supplied by scribble/manual, and doing so in a way that does not affect the output when rendered using "normal" methods like raco setup.
By default, documents written in #lang scribble/manual that are rendered to HTML link out to three files stored in the scribble collection: "manual-style.css" (which in turn brings in "manual-fonts.css" via a CSS @import declaration), and "manual-racket.css". This is all documented in Manual Rendering Style.
It’s possible to overrride "manual-style.css" by adding a #:style argument to title inside your Scribble doc; but this would affect the styling of your document every time it is rendered, which you might not want. Also, scribble/manual links "manual-racket.css" as a css-style-addition after your code runs, so there’s no way to add code to your document that can suppress that file from being included.
This module makes it easy to create a separate “themed” version of your document that imports its doc value (provided by all Scribble modules), strips out the default CSS and adds in your own. The normal/original document will still render normally, but you can also render the themed version for customized output. When you supply your main CSS file, any additional CSS files referenced via @import directives (one level deep) are automatically discovered and included.
2 Installation
Install this package from the command line:
raco pkg install scribble-theme |
You can also install it from the GitHub repository if you prefer.
3 Setting Up Your Theme
A theme is simply a CSS file (and any additional CSS files it references via @import). You can create and store your theme’s CSS files anywhere you like—typically in the same directory as your Scribble source files, or in a project subdirectory.
To get started, you can use this package’s command-line utility to generate a CSS file containing the default styles from scribble/manual:
racket -l- scribble-theme my-theme.css |
Concatenated into my-theme.css: |
/Applications/Racket v8.18/share/pkgs/scribble-lib/scribble/manual-style.css |
/Applications/Racket v8.18/share/pkgs/scribble-lib/scribble/manual-racket.css |
This gives you a starting point that you can customize.
4 Rendering HTML
In the same folder as your Scribble sources, create a new file:
"my-themed-scribblings.scrbl"
#lang racket/base (require scribble-theme) (theme/provide-doc "my-package.scrbl" "my-theme.css")
This new file acts like a custom overlay over your original Scribble doc. The first argument is the path to your original Scribble source file, and the second is the path to your main CSS file. If the CSS path is relative, it’s resolved relative to this themed file’s location.
You can render this file with scribble like so:
scribble --html +m \ |
This will place the output in the "docs/" subfolder with "index.html" as the main HTML file. Your CSS files are copied into the output under content-hashed names, such as "my-theme-3f2a9c1b.css", so browsers and CDNs fetch a fresh copy whenever the CSS changes (see css->html-defaults).
5 Adding a site navigation bar
Themed docs published on your own website usually need a way back to the rest of that site. Pass a list of navigation items to theme/provide-doc with the #:nav keyword, and a bar of links is inserted at the top of every HTML page:
"my-themed-scribblings.scrbl"
#lang racket/base (require scribble-theme) (define site-nav (list (cons "My Site" "https://example.com/") (cons "Projects" "https://example.com/projects.html") (cons "Other docs" (list (cons "Guide" "https://example.com/guide/") (cons "Reference" "https://example.com/reference/"))))) (theme/provide-doc "my-package.scrbl" "my-theme.css" #:nav site-nav)
Each item is either a nav link (a label paired with a URL) or a nav menu (a label paired with a list of nav links). A menu renders as a <details> element, so it opens and closes without JavaScript.
The bar appears exactly once on every page: on the single page produced by scribble --html, and on every page produced by scribble --htmls. Internally, a block is added to the front of every part in the document; at render time this block produces the bar only in parts that begin a new HTML page, and an empty hidden <span> everywhere else.
This package supplies no CSS for the bar. It generates the markup below, and you style it in your theme:
<nav class="theme-nav"> |
<span class="theme-nav-item"><a href="https://example.com/">My Site</a></span> |
<span class="theme-nav-item"><a href="https://example.com/projects.html">Projects</a></span> |
<details class="theme-nav-menu theme-nav-item"> |
<summary>Other docs</summary> |
<ul> |
<li><span><a href="https://example.com/guide/">Guide</a></span></li> |
<li><span><a href="https://example.com/reference/">Reference</a></span></li> |
</ul> |
</details> |
</nav> |
The bar is placed inside the main column, just after the page heading, so a fixed position is the simplest way to keep it at the top of the window. The CSS below is a starting point. It also moves Scribble’s own fixed elements (table of contents, page navigation, version box) down to make room:
:root { --site-nav-height: 2.4rem; } |
|
.theme-nav { |
position: fixed; top: 0; left: 0; right: 0; z-index: 12000; |
height: var(--site-nav-height); |
display: flex; align-items: center; |
background: #fff; border-bottom: 1px solid #eaeaea; |
} |
.theme-nav-item:first-child { margin-right: auto; } |
.theme-nav-menu { position: relative; } |
.theme-nav-menu > summary { list-style: none; cursor: pointer; } |
.theme-nav-menu ul { |
position: absolute; right: 0; top: 100%; |
list-style: none; background: #fff; border: 1px solid #eaeaea; |
} |
|
.tocset, .navsettop { top: var(--site-nav-height); } |
.maincolumn { margin-top: calc(4rem + var(--site-nav-height)); } |
.versionbox { top: calc(0.25rem + var(--site-nav-height)); } |
@media print { .theme-nav { display: none; } } |
6 Reference
syntax
(theme/provide-doc scrbl-filename css-path keyword-option ...)
keyword-option = #:nav nav-items-expr | #:fingerprint? fingerprint-expr
nav-items-expr : (or/c #f (listof nav-item/c))
fingerprint-expr : boolean?
Dynamically requires the doc value from scrbl-filename, replaces its default CSS with the CSS specified by css-path, and provides the updated doc.
The scrbl-filename should be a module path (typically a string naming a ".scrbl" file).
The css-path can be either an absolute path or a relative path. If relative, it is resolved relative to the location of the file containing the theme/provide-doc call.
Any additional CSS files referenced via @import directives in the main CSS file (one level deep) are automatically discovered and included in the output.
If #:nav is given, a site navigation bar built from the nav links and nav menus in nav-items-expr is added to the top of every HTML page (see Adding a site navigation bar).
The CSS files are copied into the output under content-hashed names unless #:fingerprint? is #f (see css->html-defaults).
This macro expands to a call to scribble/manual-custom-css wrapped in a provide that exports the doc binding.
Changed in version 2.1 of package scribble-theme: Added the #:nav and #:fingerprint? arguments.
6.1 Under the hood
You probably won’t need these functions unless you want to dynamically construct Scribble parts that use your themes.
procedure
(scribble/manual-custom-css scrbl-file new-html-defaults [ #:nav nav-items]) → part? scrbl-file : module-path? new-html-defaults : html-defaults? nav-items : (or/c #f (listof nav-item/c)) = #f
This is the function used internally by theme/provide-doc.
Changed in version 2.1 of package scribble-theme: Added the #:nav argument.
value
nav-link/c : contract? = (cons/c string? string?)
value
nav-item/c : contract?
= (or/c nav-link/c (cons/c string? (listof nav-link/c)))
Added in version 2.1 of package scribble-theme.
procedure
(theme/nav-block items) → block?
items : (listof nav-item/c)
Added in version 2.1 of package scribble-theme.
procedure
(css-imports css-file-path) → (listof absolute-path?)
css-file-path : absolute-path?
Only @import directives that reference files existing in the same directory as css-file-path are included. The function does not recursively search for @import directives in the imported files.
procedure
(css->html-defaults abs-css-path [ #:fingerprint? fingerprint?]) → html-defaults? abs-css-path : absolute-path? fingerprint? : boolean? = #t
The html-defaults uses the default Scribble prefix file and sets abs-css-path as the main style file. The extra-files field is populated with any additional CSS files discovered via css-imports.
When fingerprint? is true, the main CSS file and its imports are first copied to a "scribble-theme" folder inside the system temporary directory, each under a name that includes the first eight hex digits of the SHA-1 hash of its contents (for example, "my-theme.css" becomes "my-theme-3f2a9c1b.css"). The @import references in the copy of the main file are rewritten to the hashed names, and the copies are what Scribble installs and links in the rendered HTML. A change to any of the files produces new names, so caches never serve stale CSS; unchanged files keep the same names from build to build.
Changed in version 2.1 of package scribble-theme: Added the #:fingerprint? argument.