Skip to content

Commit 6d3ac98

Browse files
authored
Highlight bash commands (#1544)
The current bash command code blocks are highlighted inconsistently because some code blocks got detected as Ruby, so Ruby highlighting is applied. This has been addressed in #1538. <img width="60%" alt="Screenshot 2026-01-04 at 18 31 29" src="https://github.com/user-attachments/assets/8c6bec3f-f90b-4f39-b8bd-045c277ef5b6" /> But after the PR, we don't have any highlighting for bash, which I think we can provide rather simply. With this PR, we can highlight code blocks tagged as `sh`, `shell`, `bash`, and `console` with a simple JS highlighter. <img width="60%" alt="Screenshot 2026-01-04 at 18 30 02" src="https://github.com/user-attachments/assets/34090a4e-6323-4856-90ea-ac16153b7ab1" />
1 parent 296c1ab commit 6d3ac98

File tree

4 files changed

+403
-36
lines changed

4 files changed

+403
-36
lines changed

lib/rdoc/generator/template/aliki/_head.rhtml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@
140140
defer
141141
></script>
142142

143+
<script
144+
src="<%= h asset_rel_prefix %>/js/bash_highlighter.js?v=<%= h RDoc::VERSION %>"
145+
defer
146+
></script>
147+
143148
<script
144149
src="<%= h asset_rel_prefix %>/js/aliki.js?v=<%= h RDoc::VERSION %>"
145150
defer

lib/rdoc/generator/template/aliki/css/rdoc.css

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -39,24 +39,14 @@
3939
--color-neutral-800: #292524;
4040
--color-neutral-900: #1c1917;
4141

42-
/* Code highlighting colors */
42+
/* Code highlighting colors - neutral palette for all syntax highlighters */
4343
--code-blue: #1d4ed8;
4444
--code-green: #047857;
4545
--code-orange: #d97706;
4646
--code-purple: #7e22ce;
4747
--code-red: #dc2626;
48-
49-
/* C syntax highlighting */
50-
--c-keyword: #b91c1c;
51-
--c-type: #0891b2;
52-
--c-macro: #ea580c;
53-
--c-function: #7c3aed;
54-
--c-identifier: #475569;
55-
--c-operator: #059669;
56-
--c-preprocessor: #a21caf;
57-
--c-value: #92400e;
58-
--c-string: #15803d;
59-
--c-comment: #78716c;
48+
--code-cyan: #0891b2;
49+
--code-gray: #78716c;
6050

6151
/* Color Palette - Green (for success states) */
6252
--color-green-400: #4ade80;
@@ -186,24 +176,14 @@
186176

187177
/* Dark Theme */
188178
[data-theme="dark"] {
189-
/* Code highlighting colors */
179+
/* Code highlighting colors - neutral palette for all syntax highlighters */
190180
--code-blue: #93c5fd;
191181
--code-green: #34d399;
192182
--code-orange: #fbbf24;
193183
--code-purple: #c084fc;
194184
--code-red: #f87171;
195-
196-
/* C syntax highlighting */
197-
--c-keyword: #f87171;
198-
--c-type: #22d3ee;
199-
--c-macro: #fb923c;
200-
--c-function: #a78bfa;
201-
--c-identifier: #94a3b8;
202-
--c-operator: #6ee7b7;
203-
--c-preprocessor: #e879f9;
204-
--c-value: #fcd34d;
205-
--c-string: #4ade80;
206-
--c-comment: #a8a29e;
185+
--code-cyan: #22d3ee;
186+
--code-gray: #a8a29e;
207187

208188
/* Semantic Colors - Dark Theme */
209189
--color-text-primary: var(--color-neutral-50);
@@ -1064,18 +1044,30 @@ main h6 a:hover {
10641044
[data-theme="dark"] .ruby-string { color: var(--code-green); }
10651045

10661046
/* C Syntax Highlighting */
1067-
.c-keyword { color: var(--c-keyword); }
1068-
.c-type { color: var(--c-type); }
1069-
.c-macro { color: var(--c-macro); }
1070-
.c-function { color: var(--c-function); }
1071-
.c-identifier { color: var(--c-identifier); }
1072-
.c-operator { color: var(--c-operator); }
1073-
.c-preprocessor { color: var(--c-preprocessor); }
1074-
.c-value { color: var(--c-value); }
1075-
.c-string { color: var(--c-string); }
1047+
.c-keyword { color: var(--code-red); }
1048+
.c-type { color: var(--code-cyan); }
1049+
.c-macro { color: var(--code-orange); }
1050+
.c-function { color: var(--code-purple); }
1051+
.c-identifier { color: var(--color-text-secondary); }
1052+
.c-operator { color: var(--code-green); }
1053+
.c-preprocessor { color: var(--code-purple); }
1054+
.c-value { color: var(--code-orange); }
1055+
.c-string { color: var(--code-green); }
10761056

10771057
.c-comment {
1078-
color: var(--c-comment);
1058+
color: var(--code-gray);
1059+
font-style: italic;
1060+
}
1061+
1062+
/* Shell Syntax Highlighting */
1063+
.sh-prompt { color: var(--code-gray); }
1064+
.sh-command { color: var(--code-blue); }
1065+
.sh-option { color: var(--code-cyan); }
1066+
.sh-string { color: var(--code-green); }
1067+
.sh-envvar { color: var(--code-purple); }
1068+
1069+
.sh-comment {
1070+
color: var(--code-gray);
10791071
font-style: italic;
10801072
}
10811073

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
/**
2+
* Client-side shell syntax highlighter for RDoc
3+
* Highlights: $ prompts, commands, options, strings, env vars, comments
4+
*/
5+
6+
(function() {
7+
'use strict';
8+
9+
function escapeHtml(text) {
10+
return text
11+
.replace(/&/g, '&amp;')
12+
.replace(/</g, '&lt;')
13+
.replace(/>/g, '&gt;')
14+
.replace(/"/g, '&quot;')
15+
.replace(/'/g, '&#39;');
16+
}
17+
18+
function wrap(className, text) {
19+
return '<span class="' + className + '">' + escapeHtml(text) + '</span>';
20+
}
21+
22+
function highlightLine(line) {
23+
if (line.trim() === '') return escapeHtml(line);
24+
25+
var result = '';
26+
var i = 0;
27+
var len = line.length;
28+
29+
// Preserve leading whitespace
30+
while (i < len && (line[i] === ' ' || line[i] === '\t')) {
31+
result += escapeHtml(line[i++]);
32+
}
33+
34+
// Check for $ prompt ($ followed by space or end of line)
35+
if (line[i] === '$' && (line[i + 1] === ' ' || line[i + 1] === undefined)) {
36+
result += wrap('sh-prompt', '$');
37+
i++;
38+
}
39+
40+
// Check for # comment at start
41+
if (line[i] === '#') {
42+
return result + wrap('sh-comment', line.slice(i));
43+
}
44+
45+
var seenCommand = false;
46+
var afterSpace = true;
47+
48+
while (i < len) {
49+
var ch = line[i];
50+
51+
// Whitespace
52+
if (ch === ' ' || ch === '\t') {
53+
result += escapeHtml(ch);
54+
i++;
55+
afterSpace = true;
56+
continue;
57+
}
58+
59+
// Comment after whitespace
60+
if (ch === '#' && afterSpace) {
61+
result += wrap('sh-comment', line.slice(i));
62+
break;
63+
}
64+
65+
// Double-quoted string
66+
if (ch === '"') {
67+
var end = i + 1;
68+
while (end < len && line[end] !== '"') {
69+
if (line[end] === '\\' && end + 1 < len) end += 2;
70+
else end++;
71+
}
72+
if (end < len) end++;
73+
result += wrap('sh-string', line.slice(i, end));
74+
i = end;
75+
afterSpace = false;
76+
continue;
77+
}
78+
79+
// Single-quoted string
80+
if (ch === "'") {
81+
var end = i + 1;
82+
while (end < len && line[end] !== "'") end++;
83+
if (end < len) end++;
84+
result += wrap('sh-string', line.slice(i, end));
85+
i = end;
86+
afterSpace = false;
87+
continue;
88+
}
89+
90+
// Environment variable (ALLCAPS=)
91+
if (afterSpace && /[A-Z]/.test(ch)) {
92+
var match = line.slice(i).match(/^[A-Z][A-Z0-9_]*=/);
93+
if (match) {
94+
result += wrap('sh-envvar', match[0]);
95+
i += match[0].length;
96+
// Read unquoted value
97+
var valEnd = i;
98+
while (valEnd < len && line[valEnd] !== ' ' && line[valEnd] !== '\t' && line[valEnd] !== '"' && line[valEnd] !== "'") valEnd++;
99+
if (valEnd > i) {
100+
result += escapeHtml(line.slice(i, valEnd));
101+
i = valEnd;
102+
}
103+
afterSpace = false;
104+
continue;
105+
}
106+
}
107+
108+
// Option (must be after whitespace)
109+
if (ch === '-' && afterSpace) {
110+
var match = line.slice(i).match(/^--?[a-zA-Z0-9_-]+(=[^"'\s]*)?/);
111+
if (match) {
112+
result += wrap('sh-option', match[0]);
113+
i += match[0].length;
114+
afterSpace = false;
115+
continue;
116+
}
117+
}
118+
119+
// Command (first word: regular, ./path, ../path, ~/path, /abs/path, @scope/pkg)
120+
if (!seenCommand && afterSpace) {
121+
var isCmd = /[a-zA-Z0-9@~\/]/.test(ch) ||
122+
(ch === '.' && (line[i + 1] === '/' || (line[i + 1] === '.' && line[i + 2] === '/')));
123+
if (isCmd) {
124+
var end = i;
125+
while (end < len && line[end] !== ' ' && line[end] !== '\t') end++;
126+
result += wrap('sh-command', line.slice(i, end));
127+
i = end;
128+
seenCommand = true;
129+
afterSpace = false;
130+
continue;
131+
}
132+
}
133+
134+
// Everything else
135+
result += escapeHtml(ch);
136+
i++;
137+
afterSpace = false;
138+
}
139+
140+
return result;
141+
}
142+
143+
function highlightShell(code) {
144+
return code.split('\n').map(highlightLine).join('\n');
145+
}
146+
147+
function initHighlighting() {
148+
var selectors = [
149+
'pre.bash', 'pre.sh', 'pre.shell', 'pre.console',
150+
'pre[data-language="bash"]', 'pre[data-language="sh"]',
151+
'pre[data-language="shell"]', 'pre[data-language="console"]'
152+
];
153+
154+
var blocks = document.querySelectorAll(selectors.join(', '));
155+
blocks.forEach(function(block) {
156+
if (block.getAttribute('data-highlighted') === 'true') return;
157+
block.innerHTML = highlightShell(block.textContent);
158+
block.setAttribute('data-highlighted', 'true');
159+
});
160+
}
161+
162+
if (document.readyState === 'loading') {
163+
document.addEventListener('DOMContentLoaded', initHighlighting);
164+
} else {
165+
initHighlighting();
166+
}
167+
})();

0 commit comments

Comments
 (0)