The What
#![(allocator_api)]
use ::{
::{, },
::,
::,
::,
};
pub struct <: ?, : = > {
: <<>>,
: <<>>,
: ,
}
struct <: ?> {
: <>,
: <>,
: ,
}
fn () {
!("This example isn't particularly interesting to run...");
}
By the way, the sample above was created using the following options in my homegrown markup language:
#{language = "rust", file = "alloc/rc.rs", number = 4, highlight = "14,19..=20", hide = "4..=11,17,23..", tool = #["playground", "godbolt", "clipboard"], "playground-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 copying codeblock contents to the clipboard.
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"Why not data attributes? As much as I would like to just havedata-line-number=4instead of a class and a CSS variable, as of today theattr()function only works with thecontentproperty.Create a CSS counter, then use the
::beforepseudo-element to add the current line number. Using the::beforepseudo-element ensures that selecting the codeblock content with a mouse does not also select the line numbers.
code.number-lines {
: line-number-step;
: line-number-step calc(var(--number-start, 1) - 1);
& .line::before {
: inline-block;
: counter(line-number-step, decimal-leading-zero);
: line-number-step;
}
}
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:
code .line-highlight {
: rgb(92 91 94);
: 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 {
: absolute;
: -99999px;
: auto;
}
3. 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>
4. Unhide the hidden lines by resetting its position if the checkbox is checked. 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.
label.toggle-line-hidden:has(> input:checked) + code {
& .line {
--hint-color: transparent;
}
& .line-hidden {
: relative;
: auto;
}
}
5. Add CSS to show an indicator that lines are hidden:
/* When lines are numbered */
code .line-hidden+.line:not(.line-hidden)::before {
: 2px dotted var(--hint-color);
}
code .line:not(.line-hidden):has(+ .line-hidden)::before {
: 2px dotted var(--hint-color);
}
/* When lines are not numbered */
code:not(.number-lines) .line-hidden+.line:not(.line-hidden)::before {
: block;
: "";
: 4%;
: 1px;
}
code:not(.number-lines) .line:not(.line-hidden):has(+ .line-hidden)::after {
: block;
: "";
: 4%;
: 2px dotted var(--hint-color);
: 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.
Codeblock Copy to Clipboard
Also fairly simple utilizing the browser's Clipboard API. On copy success, the icon is changed to notify the user.
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
ClientStateJSON body with url-safebase64.Optional: Compress the JSON with
zlibbefore encoding.Append the body to
https://godbolt.org/clientstate/.
use :::: as _;
use :: as _;
fn (
: &mut <(&, )>,
: &,
: &,
: &,
) -> {
if != "godbolt" {
return false;
}
let = match {
"c" => {
::!({
"sessions": [{
"id": 1, "language": "c", "source": &,
"compilers": [{
"id": "cclang_trunk", "filters": { "binary": true, "execute": true },
"options": "-O0 -g -fsanitize=leak"
}],
}],
})
}
"rust" => {
::!({
"sessions": [{
"id": 1, "language": "rust", "source": &,
"compilers": [{
"id": "nightly", "filters": { "binary": true, "execute": true },
}],
}],
})
}
_ => return false,
};
let mut = ::("https://godbolt.org/clientstate/");
let mut = ::::::(::(), ::::());
.(.().()).();
let = .().();
::::.(, &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 lsifto 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">. 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). 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>. 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.
<label class="type.builtin lsp-hover-ref">
usize
<input type="button" popovertarget="11498124591756402886">
</label>
4. After rendering the page contents, render the popovers. 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...
<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>
Styling
1. 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 {
: unset;
: fixed;
: var(--space-s);
: var(--space-s);
: calc(min(48ch, 95vw) - var(--space-s));
: 40vh;
: var(--theme-background-alt);
: 2px solid var(--theme-foreground-alt);
: var(--step--1);
}
2. Using the same checkbox method as Line Hiding, the hover content can be expanded:
.lsp-hover:has(.fullscreen:checked) {
: calc(100vw - var(--space-s) * 2);
: calc(100vh - var(--space-s) * 2);
}
3. 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) {
: calc(40vh + var(--space-s) * 2);
}
4. 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 {
: text;
: pointer;
&:focus-within {
: relative;
: 1;
: var(--theme-focus) solid 2px;
}
& > input:focus-visible {
: transparent;
}
}
5. 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 {
: relative;
: auto;
}
}
6. In Firefox (not Chrome), the use of <input> causes copying and pasting the code block content (with Ctrl-c and Ctrl-v) to insert redundant, aggravating newlines, completely ruining the pasted content:
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 using tabindex="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 {
: none;
}
Conclusion
Love it? Hate it? Let me know!
Behold, a story in three posts on the orange site