Skip to content

Commit 8dfa258

Browse files
authored
Add code block syntax highlighting (#40)
Adds styled, syntax-highlighted code blocks to the theme using Prism.js. ## Changes * Styles inline code `code.css` and fenced blocks. Using token colors from the existing design. * Yellow Language Label in the top-right corner showing the language name. * Place an HTML card with <!--wrap--> before a code block to enable line wrapping instead of horizontal scroll. * Prism.js: Loaded from jsDelivr CDN with autoloader. ## Authoring In the Ghost editor, insert a Code card (via the + menu) and select a language. To enable line wrapping on a specific block, insert an HTML card with <!--wrap--> directly before it.
1 parent 61fe987 commit 8dfa258

File tree

8 files changed

+206
-1
lines changed

8 files changed

+206
-1
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ You might have to refresh by hand after each change.
5454
If you can't see your changes, and you're sure that `npm run dev` is still going, you'll likely need to disable your browser's caching.
5555
Go to the Network tab in your dev tools and toggle "Disable cache".
5656

57+
### Architecture decisions
58+
59+
Significant decisions about how the theme is built are documented in [`docs/adr/`](docs/adr/). Before making a non-trivial change — adding a dependency, changing how a feature works, picking between approaches — please write an ADR. Use [`docs/adr/template.md`](docs/adr/template.md) as a starting point. The goal isn't process for its own sake; it's making sure future contributors understand *why* things are the way they are, not just what they are.
60+
5761
### Deployment
5862

5963
The theme will automatically check, zip, and upload to Ghost via GitHub Actions. If you want to try it yourself locally:

default.hbs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
<script src="//code.jquery.com/jquery-3.6.4.min.js" defer></script>
1111
<script src="{{asset "dist/app.js"}}" defer></script>
1212

13+
{{!-- Prism syntax highlighting --}}
14+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/prism.min.js" defer></script>
15+
<script src="https://cdn.jsdelivr.net/npm/prismjs@1/plugins/autoloader/prism-autoloader.min.js" defer></script>
16+
1317
{{!-- contentFor/block scripts - use with defer --}}
1418
{{{block "scripts"}}}
1519

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ADR 0001: Code Block Syntax Highlighting
2+
3+
**Date:** 2026-03-29
4+
**Status:** Done
5+
6+
## Context
7+
8+
The theme needed support for displaying code blocks in posts and pages. Ghost's Koenig editor outputs `<pre><code class="language-*">` markup but the theme had no styles or syntax highlighting for it.
9+
10+
## Decisions
11+
12+
### Prism.js via CDN
13+
14+
Prism.js is loaded from jsDelivr with the autoloader plugin rather than bundled into `app.js`. This keeps the theme build lean and means any language is supported on demand without maintaining a list of grammar files.
15+
16+
### Token colors from the design system
17+
18+
Syntax token colors are mapped to existing CSS variables (`--color-blue`, `--color-green`, `--color-red`, `--color-yellow`) rather than introducing a third-party theme. This keeps the code blocks feeling native to the site.
19+
20+
### Language label via JS + CSS
21+
22+
Ghost's Markdown card puts `language-*` on the `<code>` element, not the `<pre>`. A script in `post.js` reads the class, extracts the language name, and sets it as `data-language` on the `<pre>`. A CSS `::before` pseudo-element renders the label. This avoids a dependency on Ghost-specific markup that could change.
23+
24+
### Line wrap toggle via `<!--wrap-->` HTML card
25+
26+
Ghost provides no mechanism to add classes to code block elements from the editor. Authors who want line wrapping instead of horizontal scroll can insert an HTML card containing `<!--wrap-->` immediately before a code block. A script in `post.js` detects the comment node and adds a `.wrap` class to the `<pre>`.
27+
28+
## Alternatives Considered
29+
30+
- **Bundling Prism grammars** — would avoid the CDN dependency but add build complexity and require updating the bundle when new languages are needed.
31+
- **A Prism theme (e.g. Tomorrow Night)** — rejected in favor of token colors derived from the design system.
32+
- **Default line wrapping** — rejected because wrapping breaks visual indentation and makes code harder to scan. Opt-in per block is more appropriate.

docs/adr/template.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# ADR XXXX: Title
2+
3+
**Date:** YYYY-MM-DD
4+
**Status:** Proposed
5+
6+
<!-- Status options:
7+
Proposed — under discussion, not yet adopted
8+
Accepted — deliberate choice made after weighing options
9+
Done — written after the fact; documents what was built and why
10+
Deprecated — no longer relevant
11+
Superseded by ADR-XXXX — replaced by a later decision
12+
-->
13+
14+
## Context
15+
16+
What problem were you solving? What constraints or forces shaped the decision?
17+
18+
## Decisions
19+
20+
What was decided, and why?
21+
22+
## Alternatives Considered
23+
24+
What else was on the table and why was it ruled out?

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "rubycentral-theme",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"description": "A Ghost theme for Ruby Central",
55
"engines": {
66
"ghost": ">=4.0.0"

src/css/app.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
/* Components */
66
@import './components/button.css';
7+
@import './components/code.css';
78
@import './components/get-involved.css';
89
@import './components/sponsors.css';
910
@import './components/profile-image.css';

src/css/components/code.css

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/* Inline code */
2+
:not(pre) > code {
3+
background-color: var(--color-lightblue);
4+
border: 1px solid color-mix(in srgb, var(--color-blue) 25%, transparent);
5+
border-radius: 4px;
6+
color: var(--color-mutednavy);
7+
font-family: 'Menlo', 'Consolas', 'Monaco', monospace;
8+
font-size: 0.875em;
9+
padding: 0.15em 0.4em;
10+
}
11+
12+
/* Code block wrapper — Ghost wraps <pre><code> in a .kg-code-card */
13+
.kg-code-card {
14+
margin: 1.75rem 0;
15+
}
16+
17+
pre {
18+
background-color: var(--color-navy);
19+
border-radius: 6px;
20+
color: #cdd9e5;
21+
font-family: 'Menlo', 'Consolas', 'Monaco', monospace;
22+
font-size: 0.875rem;
23+
line-height: 1.6;
24+
margin: 1.75rem 0;
25+
overflow-x: auto;
26+
padding: 1.25rem 1.5rem;
27+
position: relative;
28+
}
29+
30+
pre code {
31+
background: none;
32+
border: none;
33+
color: inherit;
34+
font-size: inherit;
35+
padding: 0;
36+
}
37+
38+
pre.wrap {
39+
white-space: pre-wrap;
40+
overflow-x: visible;
41+
}
42+
43+
/* Language label — set via JS from Prism's language-* class */
44+
pre[data-language]::before {
45+
background-color: var(--color-yellow);
46+
border-radius: 0 0 4px 4px;
47+
color: var(--color-navy);
48+
content: attr(data-language);
49+
font-family: 'Inter', sans-serif;
50+
font-size: 0.7rem;
51+
font-weight: 600;
52+
letter-spacing: 0.05em;
53+
padding: 2px 8px;
54+
position: absolute;
55+
right: 1rem;
56+
text-transform: uppercase;
57+
top: 0;
58+
}
59+
60+
/* Syntax token colors — tuned for the navy background */
61+
.token.comment,
62+
.token.prolog,
63+
.token.doctype,
64+
.token.cdata {
65+
color: #768390;
66+
font-style: italic;
67+
}
68+
69+
.token.punctuation {
70+
color: #8dbbe8;
71+
}
72+
73+
.token.namespace {
74+
opacity: 0.7;
75+
}
76+
77+
.token.property,
78+
.token.tag,
79+
.token.boolean,
80+
.token.number,
81+
.token.constant,
82+
.token.symbol,
83+
.token.deleted {
84+
color: var(--color-red);
85+
}
86+
87+
.token.selector,
88+
.token.attr-name,
89+
.token.string,
90+
.token.char,
91+
.token.builtin,
92+
.token.inserted {
93+
color: var(--color-green);
94+
}
95+
96+
.token.operator,
97+
.token.entity,
98+
.token.url,
99+
.language-css .token.string,
100+
.style .token.string {
101+
color: #8dbbe8;
102+
}
103+
104+
.token.atrule,
105+
.token.attr-value,
106+
.token.keyword {
107+
color: var(--color-blue);
108+
}
109+
110+
.token.function,
111+
.token.class-name {
112+
color: var(--color-yellow);
113+
}
114+
115+
.token.regex,
116+
.token.important,
117+
.token.variable {
118+
color: #f69d50;
119+
}

src/js/post/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
11
// Ship JS only active on post pages for better performance
22
import tocbot from 'tocbot';
33

4+
// Set data-language on <pre> elements so CSS can display a clean label.
5+
// Runs on load to ensure Prism's autoloader has finished highlighting.
6+
window.addEventListener('load', () => {
7+
document.querySelectorAll('pre > code[class*="language-"]').forEach((code) => {
8+
const match = code.className.match(/\blanguage-(\w+)\b/);
9+
if (match) code.closest('pre').dataset.language = match[1];
10+
});
11+
});
12+
13+
// Apply .wrap class to any <pre> immediately following a <!--wrap--> HTML card
14+
document.querySelectorAll('pre').forEach((pre) => {
15+
let node = pre.closest('.kg-code-card') ?? pre;
16+
let prev = node.previousSibling;
17+
while (prev && prev.nodeType === Node.TEXT_NODE) {
18+
prev = prev.previousSibling;
19+
}
20+
if (prev && prev.nodeType === Node.COMMENT_NODE && prev.nodeValue.trim() === 'wrap') {
21+
pre.classList.add('wrap');
22+
}
23+
});
24+
425
tocbot.init({
526
// Where to render the table of contents.
627
tocSelector: '.gh-toc',

0 commit comments

Comments
 (0)