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('', escapeHtml(comment), ''); + 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('', escapeHtml(comment), ''); + 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, '// This is a comment' + end + + def test_multi_line_comment + result = highlight('/* Multi\nline\ncomment */') + + assert_includes result, '/* Multi\nline\ncomment */' + 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, '/* Main function */' + 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, '// Print the string' + 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