sqzass.git

sqzass

~ A static site generator written in Rust
git clone https://github.com/debuman/sqzass.git

docs/content/features/highlighting.md (5597B)

      1 +++
      2 title = "Syntax highlighting"
      3 description = "Highlighted at build time, as classes, in two themes"
      4 weight = 10
      5 toc = true
      6 +++
      7 
      8 Code blocks are highlighted while the site is built. No JavaScript runs in the
      9 reader's browser to colour them, so the page is coloured on first paint.
     10 
     11 ```toml
     12 [highlight]
     13 enabled     = true
     14 theme_light = "InspiredGitHub"
     15 theme_dark  = "base16-ocean.dark"
     16 ```
     17 
     18 ## Classes, never inline styles
     19 
     20 A highlighted block looks like this:
     21 
     22 ```html
     23 <pre class="highlight"><code class="language-rust" data-lang="rust"><span class="hl-source hl-rust">…</span></code></pre>
     24 ```
     25 
     26 Not `style="color:#268bd2"`. The distinction decides three things.
     27 
     28 **Dark mode is possible at all.** Inline colours pin one theme into every
     29 document you have ever generated. Changing it means rebuilding the site;
     30 following the reader's preference means shipping both themes inside the markup.
     31 With classes, the two themes are two blocks of CSS, and switching is a CSS
     32 switch.
     33 
     34 **A strict `style-src` stays available.** A Content-Security-Policy that
     35 forbids inline styles is off the table the moment your HTML is full of them.
     36 
     37 **The stylesheet is one file.** Change a colour and every page changes, without
     38 rebuilding a single page of HTML.
     39 
     40 ## Two themes, one stylesheet
     41 
     42 `theme_light` and `theme_dark` are both compiled into the generated stylesheet.
     43 The dark rules are emitted twice — once under `prefers-color-scheme: dark` for
     44 readers who never touch a toggle, and once under `[data-theme="dark"]` for
     45 sites like this one that offer a switch.
     46 
     47 Any theme name from syntect's default set works. A name that does not exist is
     48 a build error, and the message lists the ones that do.
     49 
     50 ## The prefix
     51 
     52 Classes are prefixed `hl-`. Without a prefix, syntect emits class names like
     53 `source`, `keyword` and `string` — words general enough to collide with your
     54 own stylesheet on a site about programming.
     55 
     56 ## Marking lines and naming files
     57 
     58 Options go after the language, space-separated, as `key=value`:
     59 
     60 ````markdown
     61 ```rust hl_lines=2-3 name=src/main.rs
     62 fn main() {
     63     let marked = 2;
     64     let also_marked = 3;
     65 }
     66 ```
     67 ````
     68 
     69 And rendered, on this site's stylesheet:
     70 
     71 ```rust hl_lines=2-3 name=src/main.rs
     72 fn main() {
     73     let marked = 2;
     74     let also_marked = 3;
     75 }
     76 ```
     77 
     78 `hl_lines` takes 1-based line numbers and closed ranges, comma-separated:
     79 `hl_lines=2-4,7`. Each listed line is wrapped in `<mark class="hl-line">` —
     80 `<mark>` is visible with no CSS at all, because browsers style it out of the
     81 box, and a site restyles it with `.highlight mark`. Highlighting continues
     82 across the boundary: marking the middle line of a block comment keeps it a
     83 comment.
     84 
     85 `name` labels the block with a file name. It ships as an attribute, not as
     86 markup — `<code … data-name="src/main.rs">` — and your CSS decides whether to
     87 show it:
     88 
     89 ```css
     90 .prose pre code { display: block; width: max-content; min-width: 100%; }
     91 .prose pre code[data-name]::before {
     92   content: attr(data-name);
     93   display: block;
     94 }
     95 ```
     96 
     97 The first rule widens `code` to the longest line. It is what keeps the label —
     98 and `hl_lines` marks — running to the end of a block that scrolls sideways,
     99 instead of stopping at the visible width.
    100 
    101 Why this syntax and not Zola's `rust,hl_lines=2-4`: the comma glues the options
    102 onto the language token, which then fails syntax lookup and pollutes
    103 `class="language-…"`. After the first space the info string is free, and that
    104 is where comrak itself splits it.
    105 
    106 A typo is a build error, not a silent no-op. `hl_line=3`, `linenos=true`,
    107 `hl_lines=9` on a three-line block, a key given twice — each stops the build
    108 and names the file; the alternative is a page that ships unhighlighted while
    109 looking finished. So does a fence that starts with options instead of a
    110 language: `=` never appears in a language name, so `` ```hl_lines=2 `` is an
    111 option in the language slot, not an unknown language. The options belong to
    112 the highlighter, so with `enabled = false` they are neither applied nor
    113 checked.
    114 
    115 ## Line numbers and a copy button are yours
    116 
    117 Neither is a configuration key, and `line_numbers` was deleted rather than
    118 shipped inert — a setting that does nothing is worse than an absent one, because
    119 someone sets it and waits.
    120 
    121 Ordinary blocks carry no per-line markup to hang a CSS counter on — a wrapper
    122 around every line of every block would tax all pages for a feature most blocks
    123 never use. A gutter is a few lines of JS over the line count:
    124 
    125 ```js
    126 document.querySelectorAll(".prose pre > code").forEach(function (code) {
    127   var lines = code.textContent.split("\n").length - 1;
    128   var gutter = document.createElement("span");
    129   gutter.className = "linenos";
    130   for (var i = 1; i <= lines; i++) gutter.textContent += i + "\n";
    131   code.parentElement.prepend(gutter);
    132 });
    133 ```
    134 
    135 ```css
    136 .prose pre { display: flex; gap: 1em; }
    137 .prose pre .linenos { text-align: right; color: var(--ink-3); user-select: none; }
    138 ```
    139 
    140 A copy button is about ten lines, and the language is already on the element:
    141 
    142 ```js
    143 document.querySelectorAll(".prose pre").forEach(function (pre) {
    144   var b = document.createElement("button");
    145   b.textContent = "copy";
    146   b.addEventListener("click", function () {
    147     navigator.clipboard.writeText(pre.textContent);
    148   });
    149   pre.appendChild(b);
    150 });
    151 ```
    152 
    153 Both live in your `static/`, both are yours to restyle, and neither adds a key
    154 to a configuration file that has to keep meaning the same thing forever.
    155 
    156 ## Turning it off
    157 
    158 `enabled = false` skips highlighting and emits no stylesheet. Code blocks still
    159 get `class="language-rust"`, so a client-side highlighter can pick them up.