Skip to content

Commit f2d1892

Browse files
committed
Add C syntax highlighting with JS
1 parent d57ebf8 commit f2d1892

File tree

11 files changed

+714
-18
lines changed

11 files changed

+714
-18
lines changed

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

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)