diff --git a/lib/rdoc/generator/template/aliki/_head.rhtml b/lib/rdoc/generator/template/aliki/_head.rhtml
index cceec6af0e..8d46376d6c 100644
--- a/lib/rdoc/generator/template/aliki/_head.rhtml
+++ b/lib/rdoc/generator/template/aliki/_head.rhtml
@@ -82,6 +82,7 @@
+
diff --git a/lib/rdoc/generator/template/aliki/class.rhtml b/lib/rdoc/generator/template/aliki/class.rhtml
index 2f721a5610..9bdaa626dc 100644
--- a/lib/rdoc/generator/template/aliki/class.rhtml
+++ b/lib/rdoc/generator/template/aliki/class.rhtml
@@ -161,7 +161,7 @@
<%- if method.token_stream then %>
-
<%= method.markup_code %>
+
<%= method.markup_code %>
<%- end %>
<%- if method.mixin_from then %>
diff --git a/lib/rdoc/generator/template/aliki/css/rdoc.css b/lib/rdoc/generator/template/aliki/css/rdoc.css
index df79903e90..f063100509 100644
--- a/lib/rdoc/generator/template/aliki/css/rdoc.css
+++ b/lib/rdoc/generator/template/aliki/css/rdoc.css
@@ -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;
@@ -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);
@@ -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);
diff --git a/lib/rdoc/generator/template/aliki/js/c_highlighter.js b/lib/rdoc/generator/template/aliki/js/c_highlighter.js
new file mode 100644
index 0000000000..f5555cbb48
--- /dev/null
+++ b/lib/rdoc/generator/template/aliki/js/c_highlighter.js
@@ -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, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
+
+ /**
+ * 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('');
+ 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('');
+ 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('
', escapeHtml(preprocessor), '');
+ 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('
', escapeHtml(string), '');
+ 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('
', escapeHtml(charLit), '');
+ 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('
', escapeHtml(number), '');
+ 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('
', escapeHtml(word), '');
+ } else if (isType(word)) {
+ // Check types before macros (VALUE, ID are types, not macros)
+ tokens.push('
', escapeHtml(word), '');
+ } else if (isMacro(word)) {
+ tokens.push('
', escapeHtml(word), '');
+ } 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('
', escapeHtml(word), '');
+ } else {
+ tokens.push('
', escapeHtml(word), '');
+ }
+ }
+ 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('
', escapeHtml(op), '');
+ 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();
+ }
+})();
diff --git a/lib/rdoc/parser/c.rb b/lib/rdoc/parser/c.rb
index 7e89507779..603f28dfe1 100644
--- a/lib/rdoc/parser/c.rb
+++ b/lib/rdoc/parser/c.rb
@@ -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
@@ -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
diff --git a/lib/rdoc/parser/prism_ruby.rb b/lib/rdoc/parser/prism_ruby.rb
index 872eab9ec0..56da6ac227 100644
--- a/lib/rdoc/parser/prism_ruby.rb
+++ b/lib/rdoc/parser/prism_ruby.rb
@@ -203,7 +203,7 @@ def parse_comment_tomdoc(container, comment, line_no, start_line)
meth.call_seq = signature
return unless meth.name
- meth.start_collecting_tokens
+ meth.start_collecting_tokens(:ruby)
node = @line_nodes[line_no]
tokens = node ? visible_tokens_from_location(node.location) : [file_line_comment_token(start_line)]
tokens.each { |token| meth.token_stream << token }
@@ -554,7 +554,7 @@ def add_method(method_name, receiver_name:, receiver_fallback_type:, visibility:
meth.calls_super = calls_super
meth.block_params ||= block_params if block_params
record_location(meth)
- meth.start_collecting_tokens
+ meth.start_collecting_tokens(:ruby)
tokens.each do |token|
meth.token_stream << token
end
diff --git a/lib/rdoc/parser/ruby.rb b/lib/rdoc/parser/ruby.rb
index c97945392b..c9b662c90a 100644
--- a/lib/rdoc/parser/ruby.rb
+++ b/lib/rdoc/parser/ruby.rb
@@ -1137,7 +1137,7 @@ def parse_comment_ghost(container, text, name, column, line_no, # :nodoc:
meth = RDoc::GhostMethod.new get_tkread, name
record_location meth
- meth.start_collecting_tokens
+ meth.start_collecting_tokens(:ruby)
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
@@ -1180,7 +1180,7 @@ def parse_comment_tomdoc(container, tk, comment)
record_location meth
meth.line = line_no
- meth.start_collecting_tokens
+ meth.start_collecting_tokens(:ruby)
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
@@ -1344,7 +1344,7 @@ def parse_meta_method(container, single, tk, comment)
column = tk[:char_no]
line_no = tk[:line_no]
- start_collecting_tokens
+ start_collecting_tokens(:ruby)
add_token tk
add_token_listener self
@@ -1363,7 +1363,7 @@ def parse_meta_method(container, single, tk, comment)
remove_token_listener self
- meth.start_collecting_tokens
+ meth.start_collecting_tokens(:ruby)
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
position_comment = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
position_comment[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
@@ -1448,7 +1448,7 @@ def parse_method(container, single, tk, comment)
column = tk[:char_no]
line_no = tk[:line_no]
- start_collecting_tokens
+ start_collecting_tokens(:ruby)
add_token tk
token_listener self do
@@ -1471,7 +1471,7 @@ def parse_method(container, single, tk, comment)
record_location meth
meth.line = line_no
- meth.start_collecting_tokens
+ meth.start_collecting_tokens(:ruby)
indent = RDoc::Parser::RipperStateLex::Token.new(1, 1, :on_sp, ' ' * column)
token = RDoc::Parser::RipperStateLex::Token.new(line_no, 1, :on_comment)
token[:text] = "# File #{@top_level.relative_name}, line #{line_no}"
diff --git a/lib/rdoc/token_stream.rb b/lib/rdoc/token_stream.rb
index e4583651b1..5a4ca82a67 100644
--- a/lib/rdoc/token_stream.rb
+++ b/lib/rdoc/token_stream.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
##
# A TokenStream is a list of tokens, gathered during the parse of some entity
# (say a method). Entities populate these streams by being registered with the
@@ -87,9 +88,11 @@ def add_token(token)
##
# Starts collecting tokens
+ #
- def collect_tokens
+ def collect_tokens(language)
@token_stream = []
+ @token_stream_language = language
end
alias start_collecting_tokens collect_tokens
@@ -115,4 +118,13 @@ def tokens_to_s
(token_stream or return '').compact.map { |token| token[:text] }.join ''
end
+ ##
+ # Returns the source language of the token stream as a string
+ #
+ # Returns 'c' or 'ruby'
+
+ def source_language
+ @token_stream_language == :c ? 'c' : 'ruby'
+ end
+
end
diff --git a/test/rdoc/code_object/any_method_test.rb b/test/rdoc/code_object/any_method_test.rb
index 0c72fc479e..43dc679d95 100644
--- a/test/rdoc/code_object/any_method_test.rb
+++ b/test/rdoc/code_object/any_method_test.rb
@@ -153,7 +153,7 @@ def test_markup_code
{ :line_no => 0, :char_no => 0, :kind => :on_const, :text => 'CONSTANT' },
]
- @c2_a.collect_tokens
+ @c2_a.collect_tokens(:ruby)
@c2_a.add_tokens(tokens)
expected = '
CONSTANT'
@@ -171,7 +171,7 @@ def test_markup_code_with_line_numbers
{ :line_no => 3, :char_no => 0, :kind => :on_const, :text => 'B' }
]
- @c2_a.collect_tokens
+ @c2_a.collect_tokens(:ruby)
@c2_a.add_tokens(tokens)
assert_equal <<-EXPECTED.chomp, @c2_a.markup_code
diff --git a/test/rdoc/generator/aliki/highlight_c_test.rb b/test/rdoc/generator/aliki/highlight_c_test.rb
new file mode 100644
index 0000000000..4d7a1089db
--- /dev/null
+++ b/test/rdoc/generator/aliki/highlight_c_test.rb
@@ -0,0 +1,326 @@
+# frozen_string_literal: true
+
+require_relative '../../helper'
+
+return if RUBY_DESCRIPTION =~ /truffleruby/ || RUBY_DESCRIPTION =~ /jruby/
+
+begin
+ require 'mini_racer'
+rescue LoadError
+ return
+end
+
+class RDocGeneratorAlikiHighlightCTest < Test::Unit::TestCase
+ HIGHLIGHT_C_JS_PATH = File.expand_path(
+ '../../../../lib/rdoc/generator/template/aliki/js/c_highlighter.js',
+ __dir__
+ )
+
+ HIGHLIGHT_C_JS = begin
+ highlight_c_js = File.read(HIGHLIGHT_C_JS_PATH)
+
+ # We need to modify the JS slightly to make it work in the context of a test.
+ highlight_c_js.gsub(
+ /\(function\(\) \{[\s\S]*'use strict';/,
+ "// Test wrapper\n"
+ ).gsub(
+ /if \(document\.readyState[\s\S]*\}\)\(\);/,
+ "// Removed DOM initialization for testing"
+ )
+ end.freeze
+
+ def setup
+ @context = MiniRacer::Context.new
+ @context.eval(HIGHLIGHT_C_JS)
+ end
+
+ def teardown
+ @context.dispose
+ end
+
+ def test_keywords
+ result = highlight('int main() { return 0; }')
+
+ assert_includes result, '
int'
+ assert_includes result, '
return'
+ end
+
+ def test_identifiers
+ result = highlight('int x = 5; char *name;')
+
+ assert_includes result, '
x'
+ assert_includes result, '
name'
+ end
+
+ def test_operators
+ result = highlight('a == b && c != d')
+
+ assert_includes result, '
=='
+ assert_includes result, '
&&'
+ assert_includes result, '
!='
+ end
+
+ def test_single_char_operators
+ result = highlight('a + b - c * d / e')
+
+ assert_includes result, '
+'
+ assert_includes result, '
-'
+ assert_includes result, '
*'
+ assert_includes result, '
/'
+ end
+
+ def test_preprocessor_directives
+ result = highlight("#include
\n#define MAX 100")
+
+ assert_includes result, '#include <stdio.h>'
+ assert_includes result, '#define MAX 100'
+ end
+
+ def test_preprocessor_with_line_continuation
+ result = highlight("#define LONG_MACRO \\\n value")
+
+ # Preprocessor should capture everything including the line continuation
+ assert_includes result, '#define LONG_MACRO \\'
+ assert_includes result, ' value'
+ end
+
+ def test_single_line_comment
+ result = highlight('// This is a comment')
+
+ assert_includes result, ''
+ end
+
+ def test_multi_line_comment
+ result = highlight('/* Multi\nline\ncomment */')
+
+ assert_includes result, ''
+ end
+
+ def test_string_literals
+ result = highlight('"hello world"')
+
+ assert_includes result, '"hello world"'
+ end
+
+ def test_string_with_escapes
+ result = highlight('"hello \"world\""')
+
+ assert_includes result, '"hello \"world\""'
+ end
+
+ def test_character_literals
+ result = highlight("'a'")
+
+ assert_includes result, "'a'"
+ end
+
+ def test_character_literals_with_escapes
+ result = highlight("'\\n' '\\\\' '\\''")
+
+ assert_includes result, "'\\n'"
+ assert_includes result, "'\\\\'"
+ assert_includes result, "'\\''"
+ end
+
+ def test_decimal_numbers
+ result = highlight('42 3.14 2.5e10')
+
+ assert_includes result, '42'
+ assert_includes result, '3.14'
+ assert_includes result, '2.5e10'
+ end
+
+ def test_hexadecimal_numbers
+ result = highlight('0xFF 0xDEADBEEF')
+
+ assert_includes result, '0xFF'
+ assert_includes result, '0xDEADBEEF'
+ end
+
+ def test_octal_numbers
+ result = highlight('0755')
+
+ assert_includes result, '0755'
+ end
+
+ def test_number_suffixes
+ result = highlight('42u 3.14f 100L')
+
+ assert_includes result, '42u'
+ assert_includes result, '3.14f'
+ assert_includes result, '100L'
+ end
+
+ def test_html_escaping
+ result = highlight('if (x < 5 && y > 10) {}')
+
+ assert_includes result, '<'
+ assert_includes result, '>'
+ assert_includes result, '&&'
+ end
+
+ def test_complex_c_code
+ code = <<~C
+ #include
+
+ /* Main function */
+ int main() {
+ char *str = "Hello, World!";
+ int x = 0xFF;
+
+ // Print the string
+ if (x > 0) {
+ printf("%s\\n", str);
+ }
+ return 0;
+ }
+ C
+
+ result = highlight(code)
+
+ # Verify key components are highlighted
+ assert_includes result, '#include <stdio.h>'
+ assert_includes result, ''
+ assert_includes result, 'int'
+ assert_includes result, 'char'
+ assert_includes result, 'return'
+ assert_includes result, 'main'
+ assert_includes result, 'printf'
+ assert_includes result, '"Hello, World!"'
+ assert_includes result, '0xFF'
+ assert_includes result, '0'
+ assert_includes result, ''
+ end
+
+ def test_empty_code
+ result = highlight('')
+
+ assert_equal '', result
+ end
+
+ def test_only_whitespace
+ result = highlight(" \n\t \n ")
+
+ assert_equal " \n\t \n ", result
+ end
+
+ def test_c11_keywords
+ result = highlight('_Atomic _Bool _Complex _Thread_local')
+
+ assert_includes result, '_Atomic'
+ assert_includes result, '_Bool'
+ assert_includes result, '_Complex'
+ assert_includes result, '_Thread_local'
+ end
+
+ def test_preprocessor_only_at_line_start
+ # # in the middle of code should not be treated as preprocessor
+ result = highlight('int x = 5 # 3;')
+
+ refute_includes result, ''
+ # The # should be treated as regular text
+ assert_includes result, '#'
+ end
+
+ def test_typical_function_definition
+ code = <<~C
+ static VALUE
+ rb_ary_all_p(int argc, VALUE *argv, VALUE ary)
+ {
+ long i;
+
+ for (i = 0; i < RARRAY_LEN(ary); i++) {
+ if (!RTEST(RARRAY_AREF(ary, i))) {
+ return Qfalse;
+ }
+ }
+ return Qtrue;
+ }
+ C
+
+ result = highlight(code)
+
+ # Verify it highlights correctly without errors
+ assert_includes result, 'static'
+ assert_includes result, 'VALUE'
+ assert_includes result, 'rb_ary_all_p'
+ assert_includes result, 'int'
+ assert_includes result, 'long'
+ assert_includes result, 'for'
+ assert_includes result, 'return'
+ assert_includes result, 'Qtrue'
+ assert_includes result, 'Qfalse'
+ assert_includes result, 'RARRAY_LEN'
+ assert_includes result, 'RTEST'
+ assert_includes result, 'RARRAY_AREF'
+ end
+
+ def test_macros_all_caps
+ result = highlight('RARRAY_LEN(ary) ARY_EMBED_P(x) FL_UNSET_EMBED')
+
+ assert_includes result, 'RARRAY_LEN'
+ assert_includes result, 'ARY_EMBED_P'
+ assert_includes result, 'FL_UNSET_EMBED'
+ end
+
+ def test_types_common_ruby_types
+ result = highlight('VALUE obj; ID name; size_t len;')
+
+ assert_includes result, 'VALUE'
+ assert_includes result, 'ID'
+ assert_includes result, 'size_t'
+ end
+
+ def test_types_with_t_suffix
+ result = highlight('uint32_t count; ssize_t result; ptrdiff_t diff;')
+
+ assert_includes result, 'uint32_t'
+ assert_includes result, 'ssize_t'
+ assert_includes result, 'ptrdiff_t'
+ end
+
+ def test_function_names
+ result = highlight('rb_ary_replace(copy, orig); ary_memcpy(dest, src);')
+
+ assert_includes result, 'rb_ary_replace'
+ assert_includes result, 'ary_memcpy'
+ end
+
+ def test_function_definition
+ result = highlight('VALUE rb_ary_new(void) {')
+
+ assert_includes result, 'VALUE'
+ assert_includes result, 'rb_ary_new'
+ assert_includes result, 'void'
+ end
+
+ def test_variable_names
+ result = highlight('int count = 5; char *ptr = NULL;')
+
+ assert_includes result, 'count'
+ assert_includes result, 'ptr'
+ assert_includes result, 'NULL'
+ end
+
+ def test_mixed_identifiers_in_expression
+ result = highlight('if (ARY_EMBED_P(ary) && len > 0)')
+
+ assert_includes result, 'ARY_EMBED_P'
+ assert_includes result, 'ary'
+ assert_includes result, 'len'
+ end
+
+ def test_macro_vs_function_distinction
+ # Macros are ALL_CAPS, functions are not
+ result = highlight('RARRAY_LEN(ary) vs ary_length(ary)')
+
+ assert_includes result, 'RARRAY_LEN'
+ assert_includes result, 'ary_length'
+ end
+
+ private
+
+ def highlight(code)
+ @context.eval("highlightC(#{code.to_json})")
+ end
+end
diff --git a/test/rdoc/rdoc_token_stream_test.rb b/test/rdoc/rdoc_token_stream_test.rb
index a5f1930daa..ed5e124cc6 100644
--- a/test/rdoc/rdoc_token_stream_test.rb
+++ b/test/rdoc/rdoc_token_stream_test.rb
@@ -39,11 +39,29 @@ def test_class_to_html_empty
assert_equal '', RDoc::TokenStream.to_html([])
end
+ def test_source_language_ruby
+ foo = Class.new do
+ include RDoc::TokenStream
+ end.new
+
+ foo.collect_tokens(:ruby)
+ assert_equal 'ruby', foo.source_language
+ end
+
+ def test_source_language_c
+ foo = Class.new do
+ include RDoc::TokenStream
+ end.new
+
+ foo.collect_tokens(:c)
+ assert_equal 'c', foo.source_language
+ end
+
def test_add_tokens
foo = Class.new do
include RDoc::TokenStream
end.new
- foo.collect_tokens
+ foo.collect_tokens(:ruby)
foo.add_tokens([:token])
assert_equal [:token], foo.token_stream
end
@@ -52,7 +70,7 @@ def test_add_token
foo = Class.new do
include RDoc::TokenStream
end.new
- foo.collect_tokens
+ foo.collect_tokens(:ruby)
foo.add_token(:token)
assert_equal [:token], foo.token_stream
end
@@ -61,7 +79,7 @@ def test_collect_tokens
foo = Class.new do
include RDoc::TokenStream
end.new
- foo.collect_tokens
+ foo.collect_tokens(:ruby)
assert_equal [], foo.token_stream
end
@@ -69,7 +87,7 @@ def test_pop_token
foo = Class.new do
include RDoc::TokenStream
end.new
- foo.collect_tokens
+ foo.collect_tokens(:ruby)
foo.add_token(:token)
foo.pop_token
assert_equal [], foo.token_stream