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 Linking to docs you publish elsewhere
When a themed doc refers to a binding or section in another document (with racket, secref and so on), Scribble resolves the link from the cross-reference info of the copy installed locally. The --redirect flag shown above sends every such link through https://docs.racket-lang.org/local-redirect/, which only knows about the docs on that site. If you publish several of your own packages’ docs on your own site, links between them should stay on your site instead.
The module scribble-theme/xref loads the cross-reference info of every user-scope document itself and rewrites the link targets. Targets in the docs you publish get the absolute URL of the published page. Targets in every other user-scope doc are made to look like part of the main Racket docs, so that --redirect-main sends them to https://docs.racket-lang.org along with the real main docs. Write a small module that calls theme/load-xref with your URLs:
"site-xref.rkt"
#lang racket/base (require scribble-theme/xref) (provide site-xref) (define (site-xref) (theme/load-xref (hash "my-package" "https://example.com/docs/my-package/" "my-other-package" "https://example.com/docs/my-other-package/")))
The keys are document ids: the name of the directory that holds a document’s rendered pages, which is normally the name of the main ".scrbl" file, or the name given in the package’s scribblings declaration. Then render with ++xref-in in place of --redirect, and add --redirect-main so that links into the main Racket docs still go to https://docs.racket-lang.org:
scribble --html +m \ |
Links marked #:indirect are the one exception. Scribble always sends those through a search URL, so an indirect link into a doc you publish goes to a tag search at the installation’s documentation search site rather than to your copy. Drop the #:indirect flag for docs that are installed when you render.
7 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 : any/c
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.
7.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? : any/c = #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.
7.2 Cross-references
| (require scribble-theme/xref) | package: scribble-theme |
procedure
(theme/load-xref urls) → xref?
urls : (hash/c string? string?)
Every link target in a document whose id is a key of urls is made an absolute URL under the corresponding value. Targets in any other user-scope document are given a path under (find-doc-dir), as if the document were part of the main installation, so that scribble --redirect-main links to them the same way it links to the main docs.
Documents in the main installation are not included. Use scribble +m to load those.
Added in version 2.2 of package scribble-theme.