Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/rdoc/generator/template/aliki/_head.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
<script src="<%= h asset_rel_prefix %>/js/search.js" defer></script>
<script src="<%= h asset_rel_prefix %>/js/search_index.js" defer></script>
<script src="<%= h asset_rel_prefix %>/js/searcher.js" defer></script>
<script src="<%= h asset_rel_prefix %>/js/c_highlighter.js" defer></script>
<script src="<%= h asset_rel_prefix %>/js/aliki.js" defer></script>

<link href="<%= h asset_rel_prefix %>/css/rdoc.css" rel="stylesheet">
Expand Down
2 changes: 1 addition & 1 deletion lib/rdoc/generator/template/aliki/class.rhtml
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
<div class="method-description">
<%- if method.token_stream then %>
<div class="method-source-code" id="<%= method.html_name %>-source">
<pre><%= method.markup_code %></pre>
<pre class="<%= method.source_language %>" data-language="<%= method.source_language %>"><%= method.markup_code %></pre>
</div>
<%- end %>
<%- if method.mixin_from then %>
Expand Down
40 changes: 40 additions & 0 deletions lib/rdoc/generator/template/aliki/css/rdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@
--code-purple: #7e22ce;
--code-red: #dc2626;

/* C syntax highlighting */
--c-keyword: #b91c1c;
--c-type: #0891b2;
--c-macro: #ea580c;
--c-function: #7c3aed;
--c-identifier: #475569;
--c-operator: #059669;
--c-preprocessor: #a21caf;
--c-value: #92400e;
--c-string: #15803d;
--c-comment: #78716c;

/* Color Palette - Green (for success states) */
--color-green-400: #4ade80;
--color-green-500: #22c55e;
Expand Down Expand Up @@ -163,6 +175,18 @@
--code-purple: #c084fc;
--code-red: #f87171;

/* C syntax highlighting */
--c-keyword: #f87171;
--c-type: #22d3ee;
--c-macro: #fb923c;
--c-function: #a78bfa;
--c-identifier: #94a3b8;
--c-operator: #6ee7b7;
--c-preprocessor: #e879f9;
--c-value: #fcd34d;
--c-string: #4ade80;
--c-comment: #a8a29e;

/* Semantic Colors - Dark Theme */
--color-text-primary: var(--color-neutral-50);
--color-text-secondary: var(--color-neutral-200);
Expand Down Expand Up @@ -820,6 +844,22 @@ main h6 a:hover {
[data-theme="dark"] .ruby-value { color: var(--code-orange); }
[data-theme="dark"] .ruby-string { color: var(--code-green); }

/* C Syntax Highlighting */
.c-keyword { color: var(--c-keyword); }
.c-type { color: var(--c-type); }
.c-macro { color: var(--c-macro); }
.c-function { color: var(--c-function); }
.c-identifier { color: var(--c-identifier); }
.c-operator { color: var(--c-operator); }
.c-preprocessor { color: var(--c-preprocessor); }
.c-value { color: var(--c-value); }
.c-string { color: var(--c-string); }

.c-comment {
color: var(--c-comment);
font-style: italic;
}

/* Emphasis */
em {
text-decoration-color: var(--color-emphasis-decoration);
Expand Down
299 changes: 299 additions & 0 deletions lib/rdoc/generator/template/aliki/js/c_highlighter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,299 @@
/**
* Client-side C syntax highlighter for RDoc
*/

(function() {
'use strict';

// C control flow and storage class keywords
const C_KEYWORDS = new Set([
'auto', 'break', 'case', 'continue', 'default', 'do', 'else', 'extern',
'for', 'goto', 'if', 'inline', 'register', 'return', 'sizeof', 'static',
'switch', 'while',
'_Alignas', '_Alignof', '_Generic', '_Noreturn', '_Static_assert', '_Thread_local'
]);

// C type keywords and type qualifiers
const C_TYPE_KEYWORDS = new Set([
'bool', 'char', 'const', 'double', 'enum', 'float', 'int', 'long',
'restrict', 'short', 'signed', 'struct', 'typedef', 'union', 'unsigned',
'void', 'volatile', '_Atomic', '_Bool', '_Complex', '_Imaginary'
]);

// Library-defined types (typedef'd in headers, not language keywords)
// Includes: Ruby C API types (VALUE, ID), POSIX types (size_t, ssize_t),
// fixed-width integer types (uint32_t, int64_t), and standard I/O types (FILE)
const C_TYPES = new Set([
'VALUE', 'ID', 'size_t', 'ssize_t', 'ptrdiff_t', 'uintptr_t', 'intptr_t',
'uint8_t', 'uint16_t', 'uint32_t', 'uint64_t',
'int8_t', 'int16_t', 'int32_t', 'int64_t',
'FILE', 'DIR', 'va_list'
]);

// Common Ruby VALUE macros and boolean literals
const RUBY_MACROS = new Set([
'Qtrue', 'Qfalse', 'Qnil', 'Qundef', 'NULL', 'TRUE', 'FALSE', 'true', 'false'
]);

const OPERATORS = new Set([
'==', '!=', '<=', '>=', '&&', '||', '<<', '>>', '++', '--',
'+=', '-=', '*=', '/=', '%=', '&=', '|=', '^=', '->',
'+', '-', '*', '/', '%', '<', '>', '=', '!', '&', '|', '^', '~'
]);

// Single character that can start an operator
const OPERATOR_CHARS = new Set('+-*/%<>=!&|^~');

function isMacro(word) {
return RUBY_MACROS.has(word) || /^[A-Z][A-Z0-9_]*$/.test(word);
}

function isType(word) {
return C_TYPE_KEYWORDS.has(word) || C_TYPES.has(word) || /_t$/.test(word);
}

/**
* Escape HTML special characters
*/
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}

/**
* Check if position is at line start (only whitespace before it)
*/
function isLineStart(code, pos) {
if (pos === 0) return true;
for (let i = pos - 1; i >= 0; i--) {
const ch = code[i];
if (ch === '\n') return true;
if (ch !== ' ' && ch !== '\t') return false;
}
return true;
}

/**
* Highlight C source code
*/
function highlightC(code) {
const tokens = [];
let i = 0;
const len = code.length;

while (i < len) {
const char = code[i];

// Multi-line comment
if (char === '/' && code[i + 1] === '*') {
let end = code.indexOf('*/', i + 2);
end = (end === -1) ? len : end + 2;
const comment = code.substring(i, end);
tokens.push('<span class="c-comment">', escapeHtml(comment), '</span>');
i = end;
continue;
}

// Single-line comment
if (char === '/' && code[i + 1] === '/') {
const end = code.indexOf('\n', i);
const commentEnd = (end === -1) ? len : end;
const comment = code.substring(i, commentEnd);
tokens.push('<span class="c-comment">', escapeHtml(comment), '</span>');
i = commentEnd;
continue;
}

// Preprocessor directive (must be at line start)
if (char === '#' && isLineStart(code, i)) {
let end = i + 1;
while (end < len && code[end] !== '\n') {
if (code[end] === '\\' && end + 1 < len && code[end + 1] === '\n') {
end += 2; // Handle line continuation
} else {
end++;
}
}
const preprocessor = code.substring(i, end);
tokens.push('<span class="c-preprocessor">', escapeHtml(preprocessor), '</span>');
i = end;
continue;
}

// String literal
if (char === '"') {
let end = i + 1;
while (end < len && code[end] !== '"') {
if (code[end] === '\\' && end + 1 < len) {
end += 2; // Skip escaped character
} else {
end++;
}
}
if (end < len) end++; // Include closing quote
const string = code.substring(i, end);
tokens.push('<span class="c-string">', escapeHtml(string), '</span>');
i = end;
continue;
}

// Character literal
if (char === "'") {
let end = i + 1;
// Handle escape sequences like '\n', '\\', '\''
if (end < len && code[end] === '\\' && end + 1 < len) {
end += 2; // Skip backslash and escaped char
} else if (end < len) {
end++; // Single character
}
if (end < len && code[end] === "'") end++; // Closing quote
const charLit = code.substring(i, end);
tokens.push('<span class="c-value">', escapeHtml(charLit), '</span>');
i = end;
continue;
}

// Number (integer or float)
if (char >= '0' && char <= '9') {
let end = i;

// Hexadecimal
if (char === '0' && (code[i + 1] === 'x' || code[i + 1] === 'X')) {
end = i + 2;
while (end < len) {
const ch = code[end];
if ((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
end++;
} else {
break;
}
}
}
// Octal
else if (char === '0' && code[i + 1] >= '0' && code[i + 1] <= '7') {
end = i + 1;
while (end < len && code[end] >= '0' && code[end] <= '7') end++;
}
// Decimal/Float
else {
while (end < len) {
const ch = code[end];
if ((ch >= '0' && ch <= '9') || ch === '.') {
end++;
} else {
break;
}
}
// Scientific notation
if (end < len && (code[end] === 'e' || code[end] === 'E')) {
end++;
if (end < len && (code[end] === '+' || code[end] === '-')) end++;
while (end < len && code[end] >= '0' && code[end] <= '9') end++;
}
}

// Suffix (u, l, f, etc.)
while (end < len) {
const ch = code[end];
if (ch === 'u' || ch === 'U' || ch === 'l' || ch === 'L' || ch === 'f' || ch === 'F') {
end++;
} else {
break;
}
}

const number = code.substring(i, end);
tokens.push('<span class="c-value">', escapeHtml(number), '</span>');
i = end;
continue;
}

// Identifier or keyword
if ((char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z') || char === '_') {
let end = i + 1;
while (end < len) {
const ch = code[end];
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
(ch >= '0' && ch <= '9') || ch === '_') {
end++;
} else {
break;
}
}
const word = code.substring(i, end);

if (C_KEYWORDS.has(word)) {
tokens.push('<span class="c-keyword">', escapeHtml(word), '</span>');
} else if (isType(word)) {
// Check types before macros (VALUE, ID are types, not macros)
tokens.push('<span class="c-type">', escapeHtml(word), '</span>');
} else if (isMacro(word)) {
tokens.push('<span class="c-macro">', escapeHtml(word), '</span>');
} else {
// Check if followed by '(' -> function name
let nextCharIdx = end;
while (nextCharIdx < len && (code[nextCharIdx] === ' ' || code[nextCharIdx] === '\t')) {
nextCharIdx++;
}
if (nextCharIdx < len && code[nextCharIdx] === '(') {
tokens.push('<span class="c-function">', escapeHtml(word), '</span>');
} else {
tokens.push('<span class="c-identifier">', escapeHtml(word), '</span>');
}
}
i = end;
continue;
}

// Operators
if (OPERATOR_CHARS.has(char)) {
let op = char;
// Check for two-character operators
if (i + 1 < len) {
const twoChar = char + code[i + 1];
if (OPERATORS.has(twoChar)) {
op = twoChar;
}
}
tokens.push('<span class="c-operator">', escapeHtml(op), '</span>');
i += op.length;
continue;
}

// Everything else (punctuation, whitespace)
tokens.push(escapeHtml(char));
i++;
}

return tokens.join('');
}

/**
* Initialize C syntax highlighting on page load
*/
function initHighlighting() {
const codeBlocks = document.querySelectorAll('pre[data-language="c"]');

codeBlocks.forEach(block => {
if (block.getAttribute('data-highlighted') === 'true') {
return;
}

const code = block.textContent;
const highlighted = highlightC(code);

block.innerHTML = highlighted;
block.setAttribute('data-highlighted', 'true');
});
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initHighlighting);
} else {
initHighlighting();
}
})();
4 changes: 2 additions & 2 deletions lib/rdoc/parser/c.rb
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ def find_body(class_name, meth_name, meth_obj, file_content, quiet = false)
find_modifiers comment, meth_obj if comment

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

find_modifiers comment, meth_obj

meth_obj.start_collecting_tokens
meth_obj.start_collecting_tokens(:c)
tk = { :line_no => 1, :char_no => 1, :text => body }
meth_obj.add_token tk
meth_obj.comment = comment
Expand Down
Loading