Skip to content

Commit 602b6d7

Browse files
authored
Add syntax highlighting support + default themes (#3)
* Add syntax highlighting support + default themes * fix * fix
1 parent c47a5e4 commit 602b6d7

File tree

5 files changed

+399
-5
lines changed

5 files changed

+399
-5
lines changed

web/htdocs/assets/css/codesearch.css

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
}
5050

5151
@media (prefers-color-scheme: dark) {
52-
:root {
52+
:root:not([data-theme]) {
5353
--color-background: #0d1117;
5454
--color-background-subtle: #161b22;
5555
--color-background-hover: hsl(214, 21%, 17%);
@@ -100,6 +100,56 @@
100100
}
101101
}
102102

103+
:root[data-theme="dark"] {
104+
--color-background: #0d1117;
105+
--color-background-subtle: #161b22;
106+
--color-background-hover: hsl(214, 21%, 17%);
107+
--color-background-modal-overlay: rgba(0, 0, 0, 0.1);
108+
--color-foreground: #c9d1d9;
109+
--color-foreground-muted: hsl(215, 8%, 47%);
110+
--color-foreground-subtle: hsl(212, 9%, 58%);
111+
--color-foreground-emphasis: #ffffff;
112+
--color-shadow: rgba(0, 0, 0, 0.25);
113+
114+
--color-foreground-matchstr: inherit;
115+
--color-background-matchstr: rgb(198, 140, 40, 0.4);
116+
--color-outline-highlight-focus: rgba(187, 128, 9, 0.4);
117+
--color-background-highlight-focus: rgba(187, 128, 9, 0.15);
118+
119+
--color-foreground-symlink: rgba(60, 60, 60);
120+
--color-foreground-symlink-target: rgba(115, 115, 115);
121+
122+
--color-foreground-accent: #417dc1;
123+
--color-foreground-error: #d73a49;
124+
125+
--color-border-default: #30363d;
126+
--color-border-subtle: #6e7681;
127+
128+
--color-file-group-background: var(--color-background-subtle);
129+
--color-file-group-accent: #6e6e6e;
130+
131+
--color-prefixed-input-border: rgb(45, 51, 59);
132+
--color-prefixed-input-border-hover: hsl(208, 17%, 45%);
133+
--color-prefixed-input-border-focus: hsl(208, 47%, 44%);
134+
--color-prefixed-input-border-valid: hsl(208, 0%, 60%);
135+
--color-prefixed-input-border-valid-hover: rgb(63, 121, 168);
136+
--color-prefixed-input-border-valid-focus: rgb(72, 167, 244);
137+
138+
--color-syntax-comment: #8b949e;
139+
--color-syntax-string: #a5d6ff;
140+
--color-syntax-punctuation: #79c0ff;
141+
--color-syntax-entity: #7ee787;
142+
--color-syntax-keyword: #ff7b72;
143+
--color-syntax-function: #d2a8ff;
144+
--color-syntax-tag: #7ee787;
145+
146+
--color-bootstrap-button-background: var(--color-background-subtle);
147+
--color-bootstrap-select-background: var(--color-background-subtle);
148+
--color-bootstrap-select-background-hover: var(--color-background-hover);
149+
150+
color-scheme: dark;
151+
}
152+
103153
.bootstrap-select.btn-group .dropdown-menu .notify,
104154
.bootstrap-select.btn-group .no-results {
105155
background: var(--color-bootstrap-select-background);

web/src/codesearch/codesearch_ui.js

Lines changed: 105 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ var Cookies = require('js-cookie');
44

55
var Codesearch = require('codesearch/codesearch.js').Codesearch;
66
var RepoSelector = require('codesearch/repo_selector.js');
7+
var highlight = require('codesearch/highlight.js');
78

89
var KeyCodes = {
910
SLASH_OR_QUESTION_MARK: 191
@@ -19,6 +20,53 @@ function init(initData) {
1920
var h = new html.HTMLFactory();
2021
var last_url_update = 0;
2122

23+
var PRISM_THEMES_CDN = 'https://cdnjs.cloudflare.com/ajax/libs/prism-themes/1.9.0/';
24+
25+
// NOTE: keep in sync with the early-load script in layout.html (mode + css only)
26+
var syntaxThemes = [
27+
{key:'dracula', label:'Dracula', mode:'dark', css:'prism-dracula.min.css'},
28+
{key:'one-dark', label:'One Dark', mode:'dark', css:'prism-one-dark.min.css'},
29+
{key:'nord', label:'Nord', mode:'dark', css:'prism-nord.min.css'},
30+
{key:'solarized-dark', label:'Solarized Dark', mode:'dark', css:'prism-solarized-dark-atom.min.css'},
31+
{key:'material-dark', label:'Material Dark', mode:'dark', css:'prism-material-dark.min.css'},
32+
{key:'gruvbox-dark', label:'Gruvbox Dark', mode:'dark', css:'prism-gruvbox-dark.min.css'},
33+
{key:'vsc-dark', label:'VS Code Dark', mode:'dark', css:'prism-vsc-dark-plus.min.css'},
34+
{key:'one-light', label:'One Light', mode:'light', css:'prism-one-light.min.css'},
35+
{key:'material-light', label:'Material Light', mode:'light', css:'prism-material-light.min.css'},
36+
{key:'gruvbox-light', label:'Gruvbox Light', mode:'light', css:'prism-gruvbox-light.min.css'}
37+
];
38+
39+
var syntaxThemeMap = {};
40+
syntaxThemes.forEach(function(t) { syntaxThemeMap[t.key] = t; });
41+
42+
function applyPageMode(mode) {
43+
if (mode === 'light' || mode === 'dark') {
44+
document.documentElement.setAttribute('data-theme', mode);
45+
} else {
46+
document.documentElement.removeAttribute('data-theme');
47+
}
48+
}
49+
50+
function applySyntaxTheme(themeName) {
51+
var existing = document.getElementById('syntax-theme-css');
52+
var theme = syntaxThemeMap[themeName];
53+
if (theme) {
54+
applyPageMode(theme.mode);
55+
if (existing) {
56+
existing.href = PRISM_THEMES_CDN + theme.css;
57+
} else {
58+
var link = document.createElement('link');
59+
link.id = 'syntax-theme-css';
60+
link.rel = 'stylesheet';
61+
link.href = PRISM_THEMES_CDN + theme.css;
62+
document.head.appendChild(link);
63+
}
64+
} else {
65+
applyPageMode('auto');
66+
if (existing) existing.remove();
67+
}
68+
}
69+
2270
function vercmp(a, b) {
2371
var re = /^([0-9]*)([^0-9]*)(.*)$/;
2472
var abits, bbits;
@@ -173,20 +221,25 @@ var MatchView = Backbone.View.extend({
173221
var lno = this.model.get('lno');
174222
var ctxBefore = this.model.get('context_before'), clip_before = this.model.get('clip_before');
175223
var ctxAfter = this.model.get('context_after'), clip_after = this.model.get('clip_after');
224+
var language = this.options.language;
176225

177226
var lines_to_display_before = Math.max(0, ctxBefore.length - (clip_before || 0));
178227
for (i = 0; i < lines_to_display_before; i ++) {
228+
var ctxTextBefore = this.model.get('context_before')[i];
229+
var ctxNodesBefore = highlight.highlightContext(ctxTextBefore, language);
179230
ctx_before.unshift(
180231
this._renderLno(lno - i - 1, false),
181-
h.span([this.model.get('context_before')[i]]),
232+
ctxNodesBefore ? h.span(ctxNodesBefore) : h.span([ctxTextBefore]),
182233
h.span({}, [])
183234
);
184235
}
185236
var lines_to_display_after = Math.max(0, ctxAfter.length - (clip_after || 0));
186237
for (i = 0; i < lines_to_display_after; i ++) {
238+
var ctxTextAfter = this.model.get('context_after')[i];
239+
var ctxNodesAfter = highlight.highlightContext(ctxTextAfter, language);
187240
ctx_after.push(
188241
this._renderLno(lno + i + 1, false),
189-
h.span([this.model.get('context_after')[i]]),
242+
ctxNodesAfter ? h.span(ctxNodesAfter) : h.span([ctxTextAfter]),
190243
h.span({}, [])
191244
);
192245
}
@@ -196,6 +249,14 @@ var MatchView = Backbone.View.extend({
196249
line.substring(bounds[0], bounds[1]),
197250
line.substring(bounds[1])];
198251

252+
var highlightedNodes = highlight.highlightLine(line, language, bounds);
253+
var matchLineContent;
254+
if (highlightedNodes) {
255+
matchLineContent = h.span({cls: 'matchline'}, highlightedNodes);
256+
} else {
257+
matchLineContent = h.span({cls: 'matchline'}, [pieces[0], h.span({cls: 'matchstr'}, [pieces[1]]), pieces[2]]);
258+
}
259+
199260
var classes = ['match'];
200261
if(clip_before !== undefined) classes.push('clip-before');
201262
if(clip_after !== undefined) classes.push('clip-after');
@@ -215,7 +276,7 @@ var MatchView = Backbone.View.extend({
215276
ctx_before,
216277
[
217278
this._renderLno(lno, true),
218-
h.span({cls: 'matchline'}, [pieces[0], h.span({cls: 'matchstr'}, [pieces[1]]), pieces[2]]),
279+
matchLineContent,
219280
h.span({cls: 'matchlinks'}, links)
220281
],
221282
ctx_after
@@ -553,14 +614,30 @@ var FileGroupView = Backbone.View.extend({
553614
render: function() {
554615
var matches = this.model.matches;
555616
var el = this.$el;
617+
var self = this;
556618
el.empty();
557619
el.append(this.render_header(this.model.path_info.tree, this.model.path_info.version, this.model.path_info.path));
620+
var language = highlight.detectLanguage(this.model.path_info.path);
558621
matches.forEach(function(match) {
559622
el.append(
560-
new MatchView({model:match}).render().el
623+
new MatchView({model:match, language: language}).render().el
561624
);
562625
});
563626
el.addClass('file-group');
627+
628+
if (language && typeof Prism !== 'undefined' && !Prism.languages[language] &&
629+
Prism.plugins && Prism.plugins.autoloader && !self._loadingLanguage) {
630+
self._loadingLanguage = true;
631+
Prism.plugins.autoloader.loadLanguages([language], function() {
632+
self._loadingLanguage = false;
633+
if (Prism.languages[language]) {
634+
self.render();
635+
}
636+
}, function() {
637+
self._loadingLanguage = false;
638+
});
639+
}
640+
564641
return this;
565642
}
566643
});
@@ -787,6 +864,13 @@ var CodesearchUI = function() {
787864
CodesearchUI.inputs_case = $('input[name=fold_case]');
788865
CodesearchUI.input_regex = $('input[name=regex]');
789866
CodesearchUI.input_context = $('input[name=context]');
867+
CodesearchUI.input_syntax_theme = $('#syntax-theme');
868+
if (CodesearchUI.input_syntax_theme.length) {
869+
CodesearchUI.input_syntax_theme.append($('<option>').val('default').text('Default'));
870+
syntaxThemes.forEach(function(t) {
871+
CodesearchUI.input_syntax_theme.append($('<option>').val(t.key).text(t.label));
872+
});
873+
}
790874

791875
if (CodesearchUI.inputs_case.filter(':checked').length == 0) {
792876
CodesearchUI.inputs_case.filter('[value=auto]').attr('checked', true);
@@ -813,6 +897,12 @@ var CodesearchUI = function() {
813897
CodesearchUI.set_pref('context', CodesearchUI.input_context.prop('checked'));
814898
});
815899

900+
CodesearchUI.input_syntax_theme.change(function(){
901+
var theme = CodesearchUI.input_syntax_theme.val();
902+
applySyntaxTheme(theme);
903+
CodesearchUI.set_pref('syntaxTheme', theme);
904+
});
905+
816906
CodesearchUI.toggle_context();
817907

818908
// Defer heavy repo dropdown initialization so the page can paint first.
@@ -912,6 +1002,13 @@ var CodesearchUI = function() {
9121002
if (parms['repo[]'])
9131003
repos = repos.concat(parms['repo[]']);
9141004
RepoSelector.updateSelected(repos);
1005+
1006+
// Sync theme dropdown from saved prefs (the early-load script in
1007+
// layout.html already applies the visual theme; this syncs the control)
1008+
var prefs = Cookies.getJSON('prefs');
1009+
if (prefs && prefs['syntaxTheme'] !== undefined && CodesearchUI.input_syntax_theme.length) {
1010+
CodesearchUI.input_syntax_theme.val(prefs['syntaxTheme']);
1011+
}
9151012
},
9161013
init_controls_from_prefs: function() {
9171014
var prefs = Cookies.getJSON('prefs');
@@ -929,6 +1026,10 @@ var CodesearchUI = function() {
9291026
if (prefs['context'] !== undefined) {
9301027
CodesearchUI.input_context.prop('checked', prefs['context']);
9311028
}
1029+
if (prefs['syntaxTheme'] !== undefined && CodesearchUI.input_syntax_theme.length) {
1030+
CodesearchUI.input_syntax_theme.val(prefs['syntaxTheme']);
1031+
applySyntaxTheme(prefs['syntaxTheme']);
1032+
}
9321033
},
9331034
set_pref: function(key, value) {
9341035
// Load from the cookie again every time in case some other pref has been

0 commit comments

Comments
 (0)