Skip to content

Commit c01b7fc

Browse files
committed
Add client-side shell syntax highlighter for Aliki theme
Add a simple shell/bash syntax highlighter for documentation code blocks. The highlighter is focused on command-line documentation rather than full bash scripts. Highlights: - $ prompts (gray) - Commands/executables - first word on line (blue) - Options like -f, --flag, --option=value (cyan) - Single and double quoted strings (green) - Comments starting with # (gray, italic) The highlighter targets code blocks with language classes: bash, sh, shell, console. Files added: - lib/rdoc/generator/template/aliki/js/bash_highlighter.js - test/rdoc/generator/aliki/highlight_bash_test.rb
1 parent 1acabaf commit c01b7fc

File tree

4 files changed

+387
-0
lines changed

4 files changed

+387
-0
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: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1059,6 +1059,18 @@ main h6 a:hover {
10591059
font-style: italic;
10601060
}
10611061

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);
1071+
font-style: italic;
1072+
}
1073+
10621074
/* Emphasis */
10631075
em {
10641076
text-decoration-color: var(--color-emphasis-decoration);
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)