Skip to content

Commit bcec636

Browse files
committed
Link type names in RBS signatures to documentation pages
The JS highlighter resolves type names against the search index and wraps matches in <a> tags. Qualified names like Foo::Bar::Baz are collected as single linked units. Types not found in the index remain as unlinked <span> elements.
1 parent c3a8a62 commit bcec636

File tree

1 file changed

+116
-0
lines changed

1 file changed

+116
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
/**
2+
* Client-side RBS type signature highlighter for RDoc
3+
*
4+
* Highlights type names and built-in keywords in inline RBS annotations.
5+
* Links type names to their documentation pages using the search index.
6+
*
7+
* NOTE: innerHTML usage is safe here — input is the element's own textContent
8+
* (not user-supplied) and all output is escaped through escapeHtml(). This
9+
* follows the same pattern as c_highlighter.js and bash_highlighter.js.
10+
*/
11+
12+
(function() {
13+
'use strict';
14+
15+
var BUILTIN_TYPES = new Set([
16+
'void', 'untyped', 'nil', 'bool', 'self', 'top', 'bot',
17+
'instance', 'class', 'true', 'false'
18+
]);
19+
20+
var typeLookup = null;
21+
22+
function buildTypeLookup() {
23+
var lookup = {};
24+
if (!window.search_data || !window.search_data.index) return lookup;
25+
26+
window.search_data.index.forEach(function(entry) {
27+
if (entry.type === 'class' || entry.type === 'module') {
28+
lookup[entry.full_name] = entry.path;
29+
// Also map short name if not already taken
30+
var short = entry.name;
31+
if (!lookup[short]) lookup[short] = entry.path;
32+
}
33+
});
34+
return lookup;
35+
}
36+
37+
function escapeHtml(text) {
38+
return text
39+
.replace(/&/g, '&amp;')
40+
.replace(/</g, '&lt;')
41+
.replace(/>/g, '&gt;');
42+
}
43+
44+
function isIdentChar(ch) {
45+
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') ||
46+
(ch >= '0' && ch <= '9') || ch === '_';
47+
}
48+
49+
function highlightRbs(text) {
50+
var tokens = [];
51+
var i = 0;
52+
var len = text.length;
53+
var prefix = typeof rdoc_rel_prefix !== 'undefined' ? rdoc_rel_prefix : '';
54+
55+
while (i < len) {
56+
var ch = text[i];
57+
58+
// Uppercase identifier — possibly a type name, collect qualified name (Foo::Bar)
59+
if (ch >= 'A' && ch <= 'Z') {
60+
var end = i + 1;
61+
while (end < len && isIdentChar(text[end])) end++;
62+
// Collect :: continuations
63+
while (end + 1 < len && text[end] === ':' && text[end + 1] === ':') {
64+
end += 2;
65+
while (end < len && isIdentChar(text[end])) end++;
66+
}
67+
var name = text.substring(i, end);
68+
var href = typeLookup ? typeLookup[name] : null;
69+
70+
if (href) {
71+
tokens.push('<a href="' + prefix + href + '" class="rbs-type">' + escapeHtml(name) + '</a>');
72+
} else {
73+
tokens.push('<span class="rbs-type">' + escapeHtml(name) + '</span>');
74+
}
75+
i = end;
76+
continue;
77+
}
78+
79+
// Lowercase identifier — check for builtin keywords
80+
if ((ch >= 'a' && ch <= 'z') || ch === '_') {
81+
var end = i + 1;
82+
while (end < len && isIdentChar(text[end])) end++;
83+
var word = text.substring(i, end);
84+
85+
if (BUILTIN_TYPES.has(word)) {
86+
tokens.push('<span class="rbs-builtin">' + escapeHtml(word) + '</span>');
87+
} else {
88+
tokens.push(escapeHtml(word));
89+
}
90+
i = end;
91+
continue;
92+
}
93+
94+
tokens.push(escapeHtml(ch));
95+
i++;
96+
}
97+
98+
return tokens.join('');
99+
}
100+
101+
function initHighlighting() {
102+
typeLookup = buildTypeLookup();
103+
104+
document.querySelectorAll('.method-type-signature code').forEach(function(el) {
105+
if (el.getAttribute('data-highlighted') === 'true') return;
106+
el.innerHTML = highlightRbs(el.textContent); // eslint-disable-line no-unsanitized/property
107+
el.setAttribute('data-highlighted', 'true');
108+
});
109+
}
110+
111+
if (document.readyState === 'loading') {
112+
document.addEventListener('DOMContentLoaded', initHighlighting);
113+
} else {
114+
initHighlighting();
115+
}
116+
})();

0 commit comments

Comments
 (0)