The What
- alloc/rc.rs
#![(allocator_api)]
use std::{
alloc::{, },
cell::,
marker::,
ptr::,
};
pub struct <: ?, : = > {
: <<>>,
: <<>>,
: ,
}
struct <: ?> {
: <>,
: <>,
: ,
}
fn () {
!("This example isn't particularly interesting to run...");
}
By the way, the sample above was created by slapping this on top of my djot
codeblock:
{ ="alloc/rc.rs" number="4" highlight="14,19..=20" hide="4..=11,17,23.." tool="playground" tool="godbolt" tool-version="nightly" }
Features
- Line numbering with optional starting number
- Line highlighting, for multiple disjoint ranges
-
Line hiding, for multiple disjoint ranges
- Allow toggling visiblity of the hidden lines
- Show an indicator that some lines are hidden
- Codeblock metadata, for example the file name or the programming language used.
- Allow linking to online execution environments that contain the contents of the codeblock
- Allow IDE-like hover information for tokens through clicking.
Requirements
-
HTML used should be semantic and accessible.
-
Interactive items should work with keyboard navigation.
-
Generated HTML should look proper in Firefox Reader Mode.
-
To the greatest extent possible, all features should work without Javascript.
The How
Note: I’m going to assume you have control of the markup -> HTML pipeline in some shape or form. Personally, I use djot and a custom filter in my static site generator.
Line Numbering
-
Wrap each line of code:
<span class="line">
. -
Add a class to the containing codeblock:
<code class="number-lines">
. -
If the starting number is not 1, use a custom property to set the starting number:
style="--number-start: 4;"
[1].
-
Why not data attributes? As much as I would like to just have
data-line-number=4
instead of a class and a CSS variable, as of today theattr()
function only works with thecontent
property.
-
Create a CSS counter, then use the
::before
pseudo-element to add the current line number [2].- Minimal CSS
code.number-lines { counter-reset: line-number-step; counter-increment: line-number-step calc(var(--number-start, 1) - 1); & .line::before { display: inline-block; content: counter(line-number-step, decimal-leading-zero); counter-increment: line-number-step; } }
-
Using the
::before
pseudo-element ensures that selecting the codeblock content with a mouse does not also select the line numbers.
Line Highlighting
-
Add a class to each highlighted line of code:
<span class="line line-highlight">
. -
Change the styling of the line as desired using CSS:
- Minimal CSS
code .line-highlight { background-color: rgb(92 91 94); border-color: rgb(12 12 14); }
Line Hiding
-
Add a class to each hidden line of code:
<span class="line line-hidden">
. -
Hide the line while still making it accessible to screen readers and non-CSS environments:
code .line-hidden { position: absolute; left: -99999px; top: auto; }
-
When creating the code block, if the code block contains hidden lines, add a checkbox before the
<code>
element:<label class="toggle-line-hidden"> <input type="checkbox"> <span>Toggle N hidden lines</span> </label> <code> <span>Some code</span> </code>
-
Unhide the hidden lines by resetting its
position
if the checkbox is checked [3]:label.toggle-line-hidden:has(> input:checked) + code { & .line { --hint-color: transparent; } & .line-hidden { position: relative; left: auto; } }
-
You might have noticed that I am using fairly recent CSS features here; at the time of writing,
:has
is at 92% usage and CSS nesting is at 87% usage. This is done considering my target audience. Consider restructuring your HTML/CSS or use polyfills depending on your requirements.
-
Add CSS to show an indicator that lines are hidden:
/* When lines are numbered */ code .line-hidden+.line:not(.line-hidden)::before { border-top: 2px dotted var(--hint-color); } code .line:not(.line-hidden):has(+ .line-hidden)::before { border-bottom: 2px dotted var(--hint-color); } /* When lines are not numbered */ code:not(.number-lines) .line-hidden+.line:not(.line-hidden)::before { display: block; content: ""; width: 4%; margin-top: 1px; } code:not(.number-lines) .line:not(.line-hidden):has(+ .line-hidden)::after { display: block; content: ""; width: 4%; border-bottom: 2px dotted var(--hint-color); margin-top: 1px; }
The
--hint-color
variable is used to disable the indicator when hidden lines are shown (see the 4th step).
Codeblock Metadata
Fairly simple considering the other features, add some sort of <span>
or <ul>
element before the codeblock and style it using CSS.
Online Execution Environments
- Grab the text content of the code block.
- Turn it into a link to the execution environment using the appropriate format.
- Add the link to the information block like in Codeblock Metadata.
Example: Godbolt
Compiler Explorer allows you to open the website with a certain ClientState
loaded:
-
Encode the
ClientState
JSON body with url-safebase64
. -
Optional: Compress the JSON with
zlib
before encoding. -
Append the body to
https://godbolt.org/clientstate/
.- Example Rust Code
use std::io:: as _; use base64:: as _; fn ( : &mut <(&, )>, : &, : &, : &, ) -> { if != "godbolt" { return false; } let = match { "c" => { serde_json::!({ "sessions": [{ "id": 1, "language": "c", "source": &, "compilers": [{ "id": "cclang_trunk", "filters": { "binary": true, "execute": true }, "options": "-O0 -g -fsanitize=leak" }], }], }) } "rust" => { serde_json::!({ "sessions": [{ "id": 1, "language": "rust", "source": &, "compilers": [{ "id": "nightly", "filters": { "binary": true, "execute": true }, }], }], }) } _ => return false, }; let mut = ::("https://godbolt.org/clientstate/"); let mut = flate2::write::::(::(), flate2::::()); .(.().()).(); let = .().(); base64::prelude::.(, &mut ); .(("godbolt", )); true }
IDE Hover Information
I currently only have this feature for Rust due to relative ease of implementation. It’s also not perfect! Here’s how I do it:
Preprocessing
-
As a separate build step, crawl all Rust code blocks, and for each code block place the source code into a temporary directory, then run
rust-analyzer lsif
to generate Language Server Index Format data. - Deserialize the resulting JSON and grab the Ranges associated with all Hover results.
- Store the range -> hover mappings on disk. The mappings have to be associated with their source code blocks (I use a hash).
Rendering
- Read the mappings from disk.
- When syntax highlighting Rust source code, check if each token produced is contained within any range registered with a hover.
-
If so, wrap the token in a
<label>
with an<input type="button">
[4]. I use HTML popover elements for the “hover” information, so hash the hover content and save it for later, emitting the hash in the<input>
[5]:<label class="type.builtin lsp-hover-ref"> usize <input type="button" popovertarget="11498124591756402886"> </label>
-
Popover elements can be invoked by
<button>
or<input type="button">
elements. Initially, I went with<button>
as it seemed like no-brainer for semantics, but Firefox Reader Mode does not display any button content (with no workarounds AFAIK). -
This allows multiple labels to share the same hover content (eg. documentation for the same type used multiple times throughout the article), greatly reducing the HTML output size.
-
After rendering the page contents, render the popovers [6]:
<div id="11498124591756402886" popover class="lsp-hover"> <label> <input class="fullscreen" type="checkbox"> <span>Toggle fullscreen</span> </label> <div class="lsp-hover-content"> <pre class="highlighted"><code class="lang-rust"><span class="line"><span class="type.builtin">usize</span></span></code></pre> <hr> <p>The pointer-sized unsigned integer type.</p> <p>The size of this primitive is how many bytes it takes to reference any location in memory. For example, on a 32 bit target, this is 4 bytes and on a 64 bit target, this is 8 bytes.</p> </div> </div>
-
Since
rust-analyzer
produces hover information in Markdown, my site generator renders them as such so bold/italic text render properly, code blocks have syntax highlighting, etc…
Styling
-
Popover elements are centered by default and positioning options are limited. Until anchor positioning arrives so I can place the hover information near clicked tokens, I position popups in the bottom right corner:
.lsp-hover { inset: unset; position: fixed; bottom: var(--space-s); right: var(--space-s); max-width: calc(min(48ch, 95vw) - var(--space-s)); max-height: 40vh; background-color: var(--theme-background-alt); border: 2px solid var(--theme-foreground-alt); font-size: var(--step--1); }
-
Using the same checkbox method as Line Hiding, the hover content can be expanded:
.lsp-hover:has(.fullscreen:checked) { min-width: calc(100vw - var(--space-s) * 2); min-height: calc(100vh - var(--space-s) * 2); }
-
On smaller viewports, code blocks situated at the end of an article can be covered by the popup, so increase the bottom margin to allow scroll-positioning the code block above the popup:
article:has(> .lsp-hover:popover-open) { margin-bottom: calc(40vh + var(--space-s) * 2); }
-
The
<input>
elements can be keyboard-focused, but since they do not contain any content we have to style the parent<label>
instead..lsp-hover-ref { user-select: text; cursor: pointer; &:focus-within { position: relative; z-index: 1; outline: var(--theme-focus) solid 2px; } & > input:focus-visible { outline-color: transparent; } }
-
Since
<input>
elements can be keyboard-focused, if they are hidden there is no indication to the user. So, the following addendum to the CSS unhides hidden lines if any tokens are keyboard-focused:label.toggle-line-hidden:has(> input:checked) + code, code:has(:focus-visible) { & .line { --hint-color: transparent; } & .line-hidden { position: relative; left: auto; } }
-
In Firefox (not Chrome), the use of
<input>
causes copying and pasting the code block content (withCtrl-c
andCtrl-v
) to insert redundant, aggravating newlines, completely ruining the pasted content:- Example broken output
pub struct Rc <T : ?Sized , A : Allocator = Global > {
This can be worked around like so: For code blocks that contain hover information, the parent
<pre>
tag is made focusable usingtabindex="0"
, then the following CSS removes the<input>
elements from the layout flow if the code block is mouse-focused but not keyboard-focused:pre:focus:not(:focus-visible) .lsp-hover-ref > input { display: none; }
Conclusion
Love it? Hate it? Let me know!
Behold, a story in three posts on the orange site:
