Skip to content

Commit 33f140c

Browse files
committed
Refactor highlighter implementation
1 parent 6d1bf92 commit 33f140c

File tree

1 file changed

+86
-162
lines changed

1 file changed

+86
-162
lines changed

lib/rdoc/generator/template/aliki/js/bash_highlighter.js

Lines changed: 86 additions & 162 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
/**
22
* Client-side shell syntax highlighter for RDoc
3-
* Focused on command-line documentation (not full bash scripts)
4-
*
5-
* Highlights: $ prompts, commands (first word), options (--flag), strings, env vars (VAR=), comments (#)
3+
* Highlights: $ prompts, commands, options, strings, env vars, comments
64
*/
75

86
(function() {
97
'use strict';
108

11-
/**
12-
* Escape HTML special characters
13-
*/
149
function escapeHtml(text) {
1510
return text
1611
.replace(/&/g, '&')
@@ -20,217 +15,146 @@
2015
.replace(/'/g, ''');
2116
}
2217

23-
/**
24-
* Highlight a single line of shell code
25-
*/
18+
function wrap(className, text) {
19+
return '<span class="' + className + '">' + escapeHtml(text) + '</span>';
20+
}
21+
2622
function highlightLine(line) {
27-
if (line.trim() === '') {
28-
return escapeHtml(line);
29-
}
23+
if (line.trim() === '') return escapeHtml(line);
3024

31-
const tokens = [];
32-
let i = 0;
33-
const len = line.length;
25+
var result = '';
26+
var i = 0;
27+
var len = line.length;
3428

35-
// Skip leading whitespace
29+
// Preserve leading whitespace
3630
while (i < len && (line[i] === ' ' || line[i] === '\t')) {
37-
tokens.push(escapeHtml(line[i]));
38-
i++;
31+
result += escapeHtml(line[i++]);
3932
}
4033

41-
// Check for $ prompt at line start (after whitespace)
42-
if (i < len && line[i] === '$') {
43-
const nextChar = line[i + 1];
44-
// $ followed by space or end of line = prompt
45-
if (nextChar === ' ' || nextChar === undefined || i + 1 >= len) {
46-
tokens.push('<span class="sh-prompt">', escapeHtml('$'), '</span>');
47-
i++;
48-
}
34+
// Check for $ prompt ($ followed by space or end of line)
35+
if (line[i] === '$' && (line[i + 1] === ' ' || line[i + 1] === undefined)) {
36+
result += wrap('sh-prompt', '$');
37+
i++;
4938
}
5039

51-
// Check for # comment at line start (after whitespace)
52-
if (i < len && line[i] === '#') {
53-
// Entire rest of line is a comment
54-
tokens.push('<span class="sh-comment">', escapeHtml(line.substring(i)), '</span>');
55-
return tokens.join('');
40+
// Check for # comment at start
41+
if (line[i] === '#') {
42+
return result + wrap('sh-comment', line.slice(i));
5643
}
5744

58-
// Track if we're at a word boundary (after whitespace)
59-
let afterWhitespace = true;
60-
// Track if we've seen the command (first word) on this line
61-
let seenCommand = false;
45+
var seenCommand = false;
46+
var afterSpace = true;
6247

63-
// Process rest of line
6448
while (i < len) {
65-
const char = line[i];
49+
var ch = line[i];
6650

6751
// Whitespace
68-
if (char === ' ' || char === '\t') {
69-
tokens.push(escapeHtml(char));
52+
if (ch === ' ' || ch === '\t') {
53+
result += escapeHtml(ch);
7054
i++;
71-
afterWhitespace = true;
55+
afterSpace = true;
7256
continue;
7357
}
7458

75-
// Comment (# in middle of line, must be after whitespace)
76-
if (char === '#' && afterWhitespace) {
77-
tokens.push('<span class="sh-comment">', escapeHtml(line.substring(i)), '</span>');
59+
// Comment after whitespace
60+
if (ch === '#' && afterSpace) {
61+
result += wrap('sh-comment', line.slice(i));
7862
break;
7963
}
8064

8165
// Double-quoted string
82-
if (char === '"') {
83-
let end = i + 1;
66+
if (ch === '"') {
67+
var end = i + 1;
8468
while (end < len && line[end] !== '"') {
85-
if (line[end] === '\\' && end + 1 < len) {
86-
end += 2;
87-
} else {
88-
end++;
89-
}
69+
if (line[end] === '\\' && end + 1 < len) end += 2;
70+
else end++;
9071
}
91-
if (end < len) end++; // Include closing quote
92-
const str = line.substring(i, end);
93-
tokens.push('<span class="sh-string">', escapeHtml(str), '</span>');
72+
if (end < len) end++;
73+
result += wrap('sh-string', line.slice(i, end));
9474
i = end;
95-
afterWhitespace = false;
75+
afterSpace = false;
9676
continue;
9777
}
9878

9979
// Single-quoted string
100-
if (char === "'") {
101-
let end = i + 1;
102-
while (end < len && line[end] !== "'") {
103-
end++;
104-
}
105-
if (end < len) end++; // Include closing quote
106-
const str = line.substring(i, end);
107-
tokens.push('<span class="sh-string">', escapeHtml(str), '</span>');
80+
if (ch === "'") {
81+
var end = i + 1;
82+
while (end < len && line[end] !== "'") end++;
83+
if (end < len) end++;
84+
result += wrap('sh-string', line.slice(i, end));
10885
i = end;
109-
afterWhitespace = false;
86+
afterSpace = false;
11087
continue;
11188
}
11289

113-
// Environment variable (ALLCAPS=value, must be after whitespace)
114-
if (afterWhitespace && /[A-Z]/.test(char)) {
115-
// Look ahead to see if this is an env var pattern
116-
let end = i + 1;
117-
while (end < len && /[A-Z0-9_]/.test(line[end])) {
118-
end++;
119-
}
120-
if (end < len && line[end] === '=') {
121-
// It's an environment variable
122-
const envName = line.substring(i, end + 1); // Include =
123-
tokens.push('<span class="sh-envvar">', escapeHtml(envName), '</span>');
124-
i = end + 1;
125-
// Read value (until space, unless quoted)
126-
if (i < len && (line[i] === '"' || line[i] === "'")) {
127-
// Value is quoted, will be handled by string parsing
128-
} else {
129-
// Unquoted value
130-
let valueEnd = i;
131-
while (valueEnd < len && line[valueEnd] !== ' ' && line[valueEnd] !== '\t') {
132-
valueEnd++;
133-
}
134-
if (valueEnd > i) {
135-
const value = line.substring(i, valueEnd);
136-
tokens.push(escapeHtml(value));
137-
i = valueEnd;
138-
}
90+
// Environment variable (ALLCAPS=)
91+
if (afterSpace && /[A-Z]/.test(ch)) {
92+
var match = line.slice(i).match(/^[A-Z][A-Z0-9_]*=/);
93+
if (match) {
94+
result += wrap('sh-envvar', match[0]);
95+
i += match[0].length;
96+
// Read unquoted value
97+
var valEnd = i;
98+
while (valEnd < len && line[valEnd] !== ' ' && line[valEnd] !== '\t' && line[valEnd] !== '"' && line[valEnd] !== "'") valEnd++;
99+
if (valEnd > i) {
100+
result += escapeHtml(line.slice(i, valEnd));
101+
i = valEnd;
139102
}
140-
afterWhitespace = false;
103+
afterSpace = false;
141104
continue;
142105
}
143-
// Not an env var, fall through to command/word handling
144106
}
145107

146-
// Option (starts with -, must be after whitespace)
147-
if (char === '-' && afterWhitespace) {
148-
let end = i + 1;
149-
// Handle -- long options
150-
if (end < len && line[end] === '-') {
151-
end++;
152-
}
153-
// Read option name (letters, numbers, dashes, underscores)
154-
while (end < len && /[a-zA-Z0-9_-]/.test(line[end])) {
155-
end++;
156-
}
157-
// Handle --option=value
158-
if (end < len && line[end] === '=') {
159-
end++;
160-
// Read value (until space or end)
161-
while (end < len && line[end] !== ' ' && line[end] !== '\t') {
162-
end++;
163-
}
108+
// Option (must be after whitespace)
109+
if (ch === '-' && afterSpace) {
110+
var match = line.slice(i).match(/^--?[a-zA-Z0-9_-]+(=[^\s]*)?/);
111+
if (match) {
112+
result += wrap('sh-option', match[0]);
113+
i += match[0].length;
114+
afterSpace = false;
115+
continue;
164116
}
165-
const option = line.substring(i, end);
166-
tokens.push('<span class="sh-option">', escapeHtml(option), '</span>');
167-
i = end;
168-
afterWhitespace = false;
169-
continue;
170117
}
171118

172-
// Command (first word on line, starts with letter/number/@/~/. for paths)
173-
// Handles: command, ./script, ../script, ~/bin/cmd
174-
const isPathStart = char === '.' && i + 1 < len && (line[i + 1] === '/' || (line[i + 1] === '.' && i + 2 < len && line[i + 2] === '/'));
175-
if (!seenCommand && afterWhitespace && (/[a-zA-Z0-9@~]/.test(char) || isPathStart)) {
176-
let end = i + 1;
177-
// Read until whitespace or end
178-
while (end < len && line[end] !== ' ' && line[end] !== '\t') {
179-
end++;
119+
// Command (first word: regular, ./path, ../path, ~/path, @scope/pkg)
120+
if (!seenCommand && afterSpace) {
121+
var isCmd = /[a-zA-Z0-9@~]/.test(ch) ||
122+
(ch === '.' && (line[i + 1] === '/' || (line[i + 1] === '.' && line[i + 2] === '/')));
123+
if (isCmd) {
124+
var end = i;
125+
while (end < len && line[end] !== ' ' && line[end] !== '\t') end++;
126+
result += wrap('sh-command', line.slice(i, end));
127+
i = end;
128+
seenCommand = true;
129+
afterSpace = false;
130+
continue;
180131
}
181-
const cmd = line.substring(i, end);
182-
tokens.push('<span class="sh-command">', escapeHtml(cmd), '</span>');
183-
i = end;
184-
afterWhitespace = false;
185-
seenCommand = true;
186-
continue;
187132
}
188133

189-
// Everything else (arguments, operators, punctuation)
190-
tokens.push(escapeHtml(char));
134+
// Everything else
135+
result += escapeHtml(ch);
191136
i++;
192-
afterWhitespace = false;
137+
afterSpace = false;
193138
}
194139

195-
return tokens.join('');
140+
return result;
196141
}
197142

198-
/**
199-
* Highlight shell source code
200-
*/
201143
function highlightShell(code) {
202-
const lines = code.split('\n');
203-
const highlighted = lines.map(highlightLine);
204-
return highlighted.join('\n');
144+
return code.split('\n').map(highlightLine).join('\n');
205145
}
206146

207-
/**
208-
* Initialize shell syntax highlighting on page load
209-
*/
210147
function initHighlighting() {
211-
// Target code blocks with shell-related language classes
212-
const selectors = [
213-
'pre.bash',
214-
'pre.sh',
215-
'pre.shell',
216-
'pre.console',
217-
'pre[data-language="bash"]',
218-
'pre[data-language="sh"]',
219-
'pre[data-language="shell"]',
220-
'pre[data-language="console"]'
148+
var selectors = [
149+
'pre.bash', 'pre.sh', 'pre.shell', 'pre.console',
150+
'pre[data-language="bash"]', 'pre[data-language="sh"]',
151+
'pre[data-language="shell"]', 'pre[data-language="console"]'
221152
];
222153

223-
const codeBlocks = document.querySelectorAll(selectors.join(', '));
224-
225-
codeBlocks.forEach(block => {
226-
if (block.getAttribute('data-highlighted') === 'true') {
227-
return;
228-
}
229-
230-
const code = block.textContent;
231-
const highlighted = highlightShell(code);
232-
233-
block.innerHTML = highlighted;
154+
var blocks = document.querySelectorAll(selectors.join(', '));
155+
blocks.forEach(function(block) {
156+
if (block.getAttribute('data-highlighted') === 'true') return;
157+
block.innerHTML = highlightShell(block.textContent);
234158
block.setAttribute('data-highlighted', 'true');
235159
});
236160
}

0 commit comments

Comments
 (0)