Skip to content

Commit c8139d4

Browse files
committed
Add C syntax highlighting with JS
1 parent 7a3edb3 commit c8139d4

11 files changed

Lines changed: 708 additions & 18 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
<script src="<%= h asset_rel_prefix %>/js/search.js" defer></script>
8383
<script src="<%= h asset_rel_prefix %>/js/search_index.js" defer></script>
8484
<script src="<%= h asset_rel_prefix %>/js/searcher.js" defer></script>
85+
<script src="<%= h asset_rel_prefix %>/js/c_highlighter.js" defer></script>
8586
<script src="<%= h asset_rel_prefix %>/js/aliki.js" defer></script>
8687

8788
<link href="<%= h asset_rel_prefix %>/css/rdoc.css" rel="stylesheet">

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@
161161
<div class="method-description">
162162
<%- if method.token_stream then %>
163163
<div class="method-source-code" id="<%= method.html_name %>-source">
164-
<pre><%= method.markup_code %></pre>
164+
<pre class="<%= method.source_language %>" data-language="<%= method.source_language %>"><%= method.markup_code %></pre>
165165
</div>
166166
<%- end %>
167167
<%- if method.mixin_from then %>

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

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@
4646
--code-purple: #7e22ce;
4747
--code-red: #dc2626;
4848

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;
60+
4961
/* Color Palette - Green (for success states) */
5062
--color-green-400: #4ade80;
5163
--color-green-500: #22c55e;
@@ -163,6 +175,18 @@
163175
--code-purple: #c084fc;
164176
--code-red: #f87171;
165177

178+
/* C syntax highlighting */
179+
--c-keyword: #f87171;
180+
--c-type: #22d3ee;
181+
--c-macro: #fb923c;
182+
--c-function: #a78bfa;
183+
--c-identifier: #94a3b8;
184+
--c-operator: #6ee7b7;
185+
--c-preprocessor: #e879f9;
186+
--c-value: #fcd34d;
187+
--c-string: #4ade80;
188+
--c-comment: #a8a29e;
189+
166190
/* Semantic Colors - Dark Theme */
167191
--color-text-primary: var(--color-neutral-50);
168192
--color-text-secondary: var(--color-neutral-200);
@@ -820,6 +844,22 @@ main h6 a:hover {
820844
[data-theme="dark"] .ruby-value { color: var(--code-orange); }
821845
[data-theme="dark"] .ruby-string { color: var(--code-green); }
822846

847+
/* C Syntax Highlighting */
848+
.c-keyword { color: var(--c-keyword); }
849+
.c-type { color: var(--c-type); }
850+
.c-macro { color: var(--c-macro); }
851+
.c-function { color: var(--c-function); }
852+
.c-identifier { color: var(--c-identifier); }
853+
.c-operator { color: var(--c-operator); }
854+
.c-preprocessor { color: var(--c-preprocessor); }
855+
.c-value { color: var(--c-value); }
856+
.c-string { color: var(--c-string); }
857+
858+
.c-comment {
859+
color: var(--c-comment);
860+
font-style: italic;
861+
}
862+
823863
/* Emphasis */
824864
em {
825865
text-decoration-color: var(--color-emphasis-decoration);
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
/**
2+
* Client-side C syntax highlighter for RDoc
3+
*/
4+
5+
(function() {
6+
'use strict';
7+
8+
// C keywords
9+
const C_KEYWORDS = new Set([
10+
'auto', 'break', 'case', 'char', 'const', 'continue', 'default', 'do',
11+
'double', 'else', 'enum', 'extern', 'float', 'for', 'goto', 'if',
12+
'inline', 'int', 'long', 'register', 'restrict', 'return', 'short',
13+
'signed', 'sizeof', 'static', 'struct', 'switch', 'typedef', 'union',
14+
'unsigned', 'void', 'volatile', 'while',
15+
'_Alignas', '_Alignof', '_Atomic', '_Bool', '_Complex', '_Generic',
16+
'_Imaginary', '_Noreturn', '_Static_assert', '_Thread_local'
17+
]);
18+
19+
// Ruby-specific and standard C types
20+
const C_TYPES = new Set([
21+
'VALUE', 'ID', 'size_t', 'ssize_t', 'ptrdiff_t', 'uintptr_t', 'intptr_t',
22+
'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t',
23+
'int8_t', 'int16_t', 'int32_t', 'int64_t',
24+
'FILE', 'DIR', 'va_list'
25+
]);
26+
27+
// Common Ruby VALUE macros
28+
const RUBY_MACROS = new Set([
29+
'Qtrue', 'Qfalse', 'Qnil', 'Qundef', 'NULL'
30+
]);
31+
32+
const OPERATORS = new Set([
33+
'==', '!=', '<=', '>=', '&&', '||', '<<', '>>', '++', '--',
34+
'+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->',
35+
'+', '-', '*', '/', '%', '<', '>', '=', '!', '&', '|', '^', '~'
36+
]);
37+
38+
// Single character that can start an operator
39+
const OPERATOR_CHARS = new Set('+-*/%<>=!&|^~');
40+
41+
function isMacro(word) {
42+
return RUBY_MACROS.has(word) || /^[A-Z][A-Z0-9_]*$/.test(word);
43+
}
44+
45+
function isType(word) {
46+
return C_TYPES.has(word) || /_t$/.test(word);
47+
}
48+
49+
/**
50+
* Escape HTML special characters
51+
*/
52+
function escapeHtml(text) {
53+
return text
54+
.replace(/&/g, '&amp;')
55+
.replace(/</g, '&lt;')
56+
.replace(/>/g, '&gt;')
57+
.replace(/"/g, '&quot;')
58+
.replace(/'/g, '&#39;');
59+
}
60+
61+
/**
62+
* Check if position is at line start (only whitespace before it)
63+
*/
64+
function isLineStart(code, pos) {
65+
if (pos === 0) return true;
66+
for (let i = pos - 1; i >= 0; i--) {
67+
const ch = code[i];
68+
if (ch === '\n') return true;
69+
if (ch !== ' ' && ch !== '\t') return false;
70+
}
71+
return true;
72+
}
73+
74+
/**
75+
* Highlight C source code
76+
*/
77+
function highlightC(code) {
78+
const tokens = [];
79+
let i = 0;
80+
const len = code.length;
81+
82+
while (i < len) {
83+
const char = code[i];
84+
85+
// Multi-line comment
86+
if (char === '/' && code[i + 1] === '*') {
87+
let end = code.indexOf('*/', i + 2);
88+
end = (end === -1) ? len : end + 2;
89+
const comment = code.substring(i, end);
90+
tokens.push('<span class="c-comment">', escapeHtml(comment), '</span>');
91+
i = end;
92+
continue;
93+
}
94+
95+
// Single-line comment
96+
if (char === '/' && code[i + 1] === '/') {
97+
const end = code.indexOf('\n', i);
98+
const commentEnd = (end === -1) ? len : end;
99+
const comment = code.substring(i, commentEnd);
100+
tokens.push('<span class="c-comment">', escapeHtml(comment), '</span>');
101+
i = commentEnd;
102+
continue;
103+
}
104+
105+
// Preprocessor directive (must be at line start)
106+
if (char === '#' && isLineStart(code, i)) {
107+
let end = i + 1;
108+
while (end < len && code[end] !== '\n') {
109+
if (code[end] === '\\' && end + 1 < len && code[end + 1] === '\n') {
110+
end += 2; // Handle line continuation
111+
} else {
112+
end++;
113+
}
114+
}
115+
const preprocessor = code.substring(i, end);
116+
tokens.push('<span class="c-preprocessor">', escapeHtml(preprocessor), '</span>');
117+
i = end;
118+
continue;
119+
}
120+
121+
// String literal
122+
if (char === '"') {
123+
let end = i + 1;
124+
while (end < len && code[end] !== '"') {
125+
if (code[end] === '\\' && end + 1 < len) {
126+
end += 2; // Skip escaped character
127+
} else {
128+
end++;
129+
}
130+
}
131+
if (end < len) end++; // Include closing quote
132+
const string = code.substring(i, end);
133+
tokens.push('<span class="c-string">', escapeHtml(string), '</span>');
134+
i = end;
135+
continue;
136+
}
137+
138+
// Character literal
139+
if (char === "'") {
140+
let end = i + 1;
141+
// Handle escape sequences like '\n', '\\', '\''
142+
if (end < len && code[end] === '\\' && end + 1 < len) {
143+
end += 2; // Skip backslash and escaped char
144+
} else if (end < len) {
145+
end++; // Single character
146+
}
147+
if (end < len && code[end] === "'") end++; // Closing quote
148+
const charLit = code.substring(i, end);
149+
tokens.push('<span class="c-value">', escapeHtml(charLit), '</span>');
150+
i = end;
151+
continue;
152+
}
153+
154+
// Number (integer or float)
155+
if (char >= '0' && char <= '9') {
156+
let end = i;
157+
158+
// Hexadecimal
159+
if (char === '0' && (code[i + 1] === 'x' || code[i + 1] === 'X')) {
160+
end = i + 2;
161+
while (end < len) {
162+
const ch = code[end];
163+
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
164+
end++;
165+
} else {
166+
break;
167+
}
168+
}
169+
}
170+
// Octal
171+
else if (char === '0' && code[i + 1] >= '0' && code[i + 1] <= '7') {
172+
end = i + 1;
173+
while (end < len && code[end] >= '0' && code[end] <= '7') end++;
174+
}
175+
// Decimal/Float
176+
else {
177+
while (end < len) {
178+
const ch = code[end];
179+
if ((ch >= '0' && ch <= '9') || ch === '.') {
180+
end++;
181+
} else {
182+
break;
183+
}
184+
}
185+
// Scientific notation
186+
if (end < len && (code[end] === 'e' || code[end] === 'E')) {
187+
end++;
188+
if (end < len && (code[end] === '+' || code[end] === '-')) end++;
189+
while (end < len && code[end] >= '0' && code[end] <= '9') end++;
190+
}
191+
}
192+
193+
// Suffix (u, l, f, etc.)
194+
while (end < len) {
195+
const ch = code[end];
196+
if (ch === 'u' || ch === 'U' || ch === 'l' || ch === 'L' || ch === 'f' || ch === 'F') {
197+
end++;
198+
} else {
199+
break;
200+
}
201+
}
202+
203+
const number = code.substring(i, end);
204+
tokens.push('<span class="c-value">', escapeHtml(number), '</span>');
205+
i = end;
206+
continue;
207+
}
208+
209+
// Identifier or keyword
210+
if ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_') {
211+
let end = i + 1;
212+
while (end < len) {
213+
const ch = code[end];
214+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
215+
(ch >= '0' && ch <= '9') || ch === '_') {
216+
end++;
217+
} else {
218+
break;
219+
}
220+
}
221+
const word = code.substring(i, end);
222+
223+
if (C_KEYWORDS.has(word)) {
224+
tokens.push('<span class="c-keyword">', escapeHtml(word), '</span>');
225+
} else if (isType(word)) {
226+
// Check types before macros (VALUE, ID are types, not macros)
227+
tokens.push('<span class="c-type">', escapeHtml(word), '</span>');
228+
} else if (isMacro(word)) {
229+
tokens.push('<span class="c-macro">', escapeHtml(word), '</span>');
230+
} else {
231+
// Check if followed by '(' -> function name
232+
let nextCharIdx = end;
233+
while (nextCharIdx < len && (code[nextCharIdx] === ' ' || code[nextCharIdx] === '\t')) {
234+
nextCharIdx++;
235+
}
236+
if (nextCharIdx < len && code[nextCharIdx] === '(') {
237+
tokens.push('<span class="c-function">', escapeHtml(word), '</span>');
238+
} else {
239+
tokens.push('<span class="c-identifier">', escapeHtml(word), '</span>');
240+
}
241+
}
242+
i = end;
243+
continue;
244+
}
245+
246+
// Operators
247+
if (OPERATOR_CHARS.has(char)) {
248+
let op = char;
249+
// Check for two-character operators
250+
if (i + 1 < len) {
251+
const twoChar = char + code[i + 1];
252+
if (OPERATORS.has(twoChar)) {
253+
op = twoChar;
254+
}
255+
}
256+
tokens.push('<span class="c-operator">', escapeHtml(op), '</span>');
257+
i += op.length;
258+
continue;
259+
}
260+
261+
// Everything else (punctuation, whitespace)
262+
tokens.push(escapeHtml(char));
263+
i++;
264+
}
265+
266+
return tokens.join('');
267+
}
268+
269+
/**
270+
* Initialize C syntax highlighting on page load
271+
*/
272+
function initHighlighting() {
273+
const codeBlocks = document.querySelectorAll('pre[data-language="c"]');
274+
275+
codeBlocks.forEach(block => {
276+
if (block.getAttribute('data-highlighted') === 'true') {
277+
return;
278+
}
279+
280+
const code = block.textContent;
281+
const highlighted = highlightC(code);
282+
283+
block.innerHTML = highlighted;
284+
block.setAttribute('data-highlighted', 'true');
285+
});
286+
}
287+
288+
if (document.readyState === 'loading') {
289+
document.addEventListener('DOMContentLoaded', initHighlighting);
290+
} else {
291+
initHighlighting();
292+
}
293+
})();

lib/rdoc/parser/c.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
622622
find_modifiers comment, meth_obj if comment
623623

624624
#meth_obj.params = params
625-
meth_obj.start_collecting_tokens
625+
meth_obj.start_collecting_tokens(:c)
626626
tk = { :line_no => 1, :char_no => 1, :text => body }
627627
meth_obj.add_token tk
628628
meth_obj.comment = comment
@@ -638,7 +638,7 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
638638

639639
find_modifiers comment, meth_obj
640640

641-
meth_obj.start_collecting_tokens
641+
meth_obj.start_collecting_tokens(:c)
642642
tk = { :line_no => 1, :char_no => 1, :text => body }
643643
meth_obj.add_token tk
644644
meth_obj.comment = comment

0 commit comments

Comments
 (0)