blob: 34927d068bbbf197132752b881325d276a09b23f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
/*
 * The client side script that cgit serves with every page. Its main job is
 * to refresh the relative ages that cgit_print_age renders in
 * source/ui-shared.c, so the thresholds, suffixes and class names tabulated
 * below mirror the constants there and the two have to move together. Three
 * smaller pieces follow, the selects that reload the page when they change,
 * the highlight laid over the source line a URL fragment names, and the
 * colour theme toggle. Each piece is wrapped in a function of its own so
 * that nothing is left behind in the global scope.
 */

(function () {

// Written the way SECONDS_PER_* are derived in source/cgit.h, so that the
// bucket a browser picks is the bucket cgit_print_age already picked on the
// server. A month is a twelfth of a year rather than thirty days, which is
// the definition the server uses.
var MINUTE = 60;
var HOUR   = 60 * MINUTE;
var DAY    = 24 * HOUR;
var WEEK   = 7 * DAY;
var YEAR   = 365 * DAY;
var MONTH  = YEAR / 12;

// The five arrays are indexed together by the bucket an age falls into. The
// search in render_age can stop one place past the last class, so the arrays
// it reads there repeat their final entry. Each limit is twice the next unit
// up, matching the thresholds cgit_print_age compares against.
var age_classes  = [ "age-mins", "age-hours", "age-days", "age-weeks", "age-months", "age-years" ];
var age_suffix   = [ "min.",     "hours",     "days",     "weeks",     "months",     "years",     "years" ];
var age_unit     = [ MINUTE,     HOUR,        DAY,        WEEK,        MONTH,        YEAR,        YEAR ];
var age_limit    = [ 2 * HOUR,   2 * DAY,     2 * WEEK,   2 * MONTH,   2 * YEAR,     2 * YEAR ];
var update_delay = [ 10,         5 * MINUTE,  30 * MINUTE, DAY,        DAY,          DAY,         DAY ];

function render_age(element, age) {
  var text, bucket;

  for (bucket = 0; bucket < age_classes.length; bucket++)
    if (age < age_limit[bucket])
      break;

  text = Math.round(age / age_unit[bucket]) + " " + age_suffix[bucket];

  // Every age on the page is measured again on each pass, so most of
  // them are already reading correctly and writing to them would cost a
  // repaint for nothing.
  if (element.textContent != text) {
    element.textContent = text;
    // An age past the last limit is still counted in years, but
    // there is no class beyond age-years to put on it.
    if (bucket == age_classes.length)
      bucket--;
    if (element.className != age_classes[bucket])
      element.className = age_classes[bucket];
  }
}

/*
 * Measures every age on the page against the clock and books the next pass.
 * There is no point coming back before the coarsest unit on the page could
 * change, so a page already counted in hours is left alone for minutes and
 * one counted in years for a day.
 */
function refresh_ages() {
  var bucket, elements, i, age;
  var delay = 24 * 3600;
  var now = Math.round(new Date().getTime() / 1000);

  for (bucket = 0; bucket < age_classes.length; bucket++) {
    elements = document.getElementsByClassName(age_classes[bucket]);

    if (elements.length && update_delay[bucket] < delay)
      delay = update_delay[bucket];

    for (i = 0; i < elements.length; i++) {
      age = now - elements[i].getAttribute("data-ut");

      // A commit dated ahead of the viewer's clock would
      // otherwise show a negative age, and ui-shared.c
      // clamps it the same way.
      if (age < 0)
        age = 0;

      render_age(elements[i], age);
    }
  }

  window.setTimeout(refresh_ages, delay * 1000);
}

document.addEventListener("DOMContentLoaded", function () {
  // Nothing here depends on layout, so the first pass can run as soon
  // as the document is parsed.
  refresh_ages();
}, false);

})();

/*
 * Selects marked data-autosubmit reload the page with the new setting. They
 * are wired up from here rather than with an inline onchange handler because
 * a strict Content-Security-Policy blocks those, and a reader without
 * scripting still has the noscript reload button the forms carry.
 */

(function () {

document.addEventListener("DOMContentLoaded", function () {
  var i, selects = document.querySelectorAll("select[data-autosubmit]");

  for (i = 0; i < selects.length; i++)
    selects[i].addEventListener("change", function () {
      this.form.submit();
    });
}, false);

})();

/*
 * Washes a faded highlight over the source line a URL fragment names, either
 * a single line as in #n231 or a range as in #n5-n12. The line anchors live
 * in the number gutter, so the anchor is measured and a bar of the same
 * height is laid across the full width of the code. Without scripting the
 * CSS target rule still tints the line number itself.
 */

(function () {

var bar = null;

function place_bar() {
  var match, first, last, table;
  var table_box, first_box, last_box, top, bottom;

  if (bar) {
    bar.remove();
    bar = null;
  }
  match = /^#n(\d+)(?:-n?(\d+))?$/.exec(location.hash);
  if (!match)
    return;
  first = document.getElementById("n" + match[1]);
  if (!first)
    return;
  last = (match[2] && document.getElementById("n" + match[2])) || first;
  table = first.closest("table");
  if (!table)
    return;
  table_box = table.getBoundingClientRect();
  first_box = first.getBoundingClientRect();
  last_box = last.getBoundingClientRect();
  top = Math.min(first_box.top, last_box.top) - table_box.top;
  bottom = Math.max(first_box.bottom, last_box.bottom) - table_box.top;
  bar = document.createElement("div");
  bar.className = "line-hl";
  bar.style.top = top + "px";
  bar.style.height = (bottom - top) + "px";
  table.appendChild(bar);

  // A browser only scrolls to a fragment that names a real id, which a
  // range never does, so bring the start of one into view here.
  if (match[2])
    first.scrollIntoView({ block: "center" });
}

document.addEventListener("DOMContentLoaded", place_bar, false);
window.addEventListener("hashchange", place_bar, false);

})();

/*
 * The colour theme toggle, which cycles from auto through light to dark and
 * remembers the choice. Auto sets no override at all, leaving the page to
 * follow the system preference the way the stylesheet does on its own.
 */

(function () {

var STORAGE_KEY = "cgit-theme";
var ORDER = [ "auto", "light", "dark" ];
var ICONS = {
  auto:  '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="9"/><path d="M12 3a9 9 0 0 0 0 18z" fill="currentColor" stroke="none"/></svg>',
  light: '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="5"/><path d="M12 1v3M12 20v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M1 12h3M20 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/></svg>',
  dark:  '<svg viewBox="0 0 24 24"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8z"/></svg>'
};

/*
 * Local storage throws instead of answering when the browser has storage
 * switched off for the site, and a colour preference is not worth breaking
 * the page over, so both of these swallow the failure and the reader is left
 * on the automatic theme.
 */
function saved() {
  try {
    return localStorage.getItem(STORAGE_KEY);
  } catch (e) {
    return null;
  }
}

function persist(theme) {
  try {
    localStorage.setItem(STORAGE_KEY, theme);
  } catch (e) { }
}

function apply(theme, button) {
  var html = document.documentElement;

  if (theme === "auto")
    html.removeAttribute("data-theme");
  else
    html.setAttribute("data-theme", theme);

  // The browser paints the page canvas and the scrollbars from
  // color-scheme rather than from the stylesheet, so it has to be told
  // the choice as well.
  html.style.colorScheme = (theme === "auto") ? "" : theme;

  if (button) {
    button.innerHTML = ICONS[theme];
    button.title = "Colour theme: " + theme;
  }
}

var theme = saved();
if (ORDER.indexOf(theme) < 0)
  theme = "auto";

// This runs while the file is being parsed rather than on DOMContentLoaded.
// The script is fetched from the head and so runs before the body exists,
// which lands the choice ahead of the first paint and stops the page showing
// the other theme for a moment. The marker goes on the same element for the
// same reason, since div#cgit has not been parsed yet, and cgit-js reserves
// the toggle's column so that revealing the button below does not shift the
// header sideways.
apply(theme, null);
document.documentElement.classList.add("cgit-js");

document.addEventListener("DOMContentLoaded", function () {
  var root = document.getElementById("cgit");
  if (!root)
    return;
  var button = root.querySelector(".theme-toggle");
  if (!button)
    return;

  apply(theme, button);
  // The button is served hidden so that a reader without scripting is
  // never shown a control that cannot do anything.
  button.hidden = false;
  button.addEventListener("click", function () {
    theme = ORDER[(ORDER.indexOf(theme) + 1) % ORDER.length];
    persist(theme);
    apply(theme, button);
  });
}, false);

})();