PageSourceSearch

https://disniverse.com/pintrading/pintrading-comments.js

js disniverse.com collected 2026-10-09 05:20:14 UTC 24,012 bytes, 733 lines download raw bytes

1(function () {
2  'use strict';
3
4  var API = '/pintrading/api';
5
6  function lang() {
7    return document.body.getAttribute('data-guide-lang') || document.documentElement.lang || 'en';
8  }
9
10  var STR = {
11    en: {
12      lead: 'Sign in to join the discussion. New posts are reviewed by server admins before they appear.',
13      loginDiscord: 'Discord',
14      loginGoogle: 'Google',
15      logout: 'Sign out',
16      placeholder: 'Write a comment…',
17      hint: 'Markdown: **bold**, *italic*, `code`, [label](https://…)',
18      submit: 'Submit',
19      cancelReply: 'Cancel reply',
20      reply: 'Reply',
21      replyingTo: 'Replying to thread',
22      none: 'No comments yet. Be the first to share a tip or a question.',
23      modTitle: 'Moderation queue',
24      modEmpty: 'No comments awaiting approval.',
25      approve: 'Approve',
26      reject: 'Reject',
27      removePublic: 'Remove',
28      removePublicConfirm: 'Remove this comment from the public page?',
29      modHintGoogleOnly:
30        'Moderation is tied to Discord. Sign out and sign in with Discord to use the moderation queue.',
31      modHintDiscordApi:
32        'You are on Discord, but moderator access failed. Enable Server Members Intent for the bot in the Discord Developer Portal, or ask the site owner to list your Discord user ID in extra_moderator_user_ids.',
33      pendingNote: 'Comments appear after moderator approval.',
34      submitted: 'Thanks. Your comment was sent for review.',
35      errorGeneric: 'Something went wrong. Please try again.',
36      authOk: 'You are signed in.',
37      authErr: 'Sign-in failed.',
38      signedInVia: 'Signed in',
39    },
40    fr: {
41      lead: 'Connectez-vous pour participer. Les nouveaux messages sont vérifiés par les admins du serveur avant publication.',
42      loginDiscord: 'Discord',
43      loginGoogle: 'Google',
44      logout: 'Se déconnecter',
45      placeholder: 'Écrire un commentaire…',
46      hint: 'Markdown : **gras**, *italique*, `code`, [texte](https://…)',
47      submit: 'Envoyer',
48      cancelReply: 'Annuler la réponse',
49      reply: 'Répondre',
50      replyingTo: 'Réponse dans le fil',
51      none: 'Pas encore de commentaires.',
52      modTitle: 'File de modération',
53      modEmpty: 'Aucun commentaire en attente.',
54      approve: 'Approuver',
55      reject: 'Refuser',
56      removePublic: 'Retirer',
57      removePublicConfirm: 'Retirer ce commentaire de la page publique ?',
58      modHintGoogleOnly:
59        'La modération est liée à Discord. Déconnectez-vous et reconnectez-vous avec Discord pour utiliser la file.',
60      modHintDiscordApi:
61        'Vous êtes sur Discord, mais l’accès modérateur a échoué. Activez l’intent « Server Members » pour le bot dans le portail développeur Discord, ou demandez d’ajouter votre ID utilisateur dans extra_moderator_user_ids.',
62      pendingNote: 'Les commentaires apparaissent après validation par un modérateur.',
63      submitted: 'Merci. Votre commentaire a été envoyé pour relecture.',
64      errorGeneric: 'Une erreur est survenue. Réessayez.',
65      authOk: 'Vous êtes connecté·e.',
66      authErr: 'Connexion impossible.',
67      signedInVia: 'Connecté via',
68    },
69    nl: {
70      lead: 'Log in om mee te praten. Nieuwe berichten worden door serveradmins goedgekeurd voordat ze zichtbaar zijn.',
71      loginDiscord: 'Discord',
72      loginGoogle: 'Google',
73      logout: 'Uitloggen',
74      placeholder: 'Schrijf een reactie…',
75      hint: 'Markdown: **vet**, *cursief*, `code`, [tekst](https://…)',
76      submit: 'Versturen',
77      cancelReply: 'Antwoord annuleren',
78      reply: 'Antwoorden',
79      replyingTo: 'Antwoord in draad',
80      none: 'Nog geen reacties.',
81      modTitle: 'Moderatiewachtrij',
82      modEmpty: 'Geen reacties in afwachting.',
83      approve: 'Goedkeuren',
84      reject: 'Afwijzen',
85      removePublic: 'Verwijderen',
86      removePublicConfirm: 'Deze reactie van de openbare pagina verwijderen?',
87      modHintGoogleOnly:
88        'Moderatie hangt aan Discord. Log uit en log opnieuw in met Discord om de moderatiewachtrij te gebruiken.',
89      modHintDiscordApi:
90        'Je bent op Discord, maar moderatorrechten lukken niet. Zet « Server Members Intent » aan voor de bot in het Discord Developer Portal, of laat je Discord user ID in extra_moderator_user_ids zetten.',
91      pendingNote: 'Reacties verschijnen na goedkeuring door een moderator.',
92      submitted: 'Bedankt. Je reactie is ter beoordeling verstuurd.',
93      errorGeneric: 'Er ging iets mis. Probeer opnieuw.',
94      authOk: 'Je bent ingelogd.',
95      authErr: 'Inloggen mislukt.',
96      signedInVia: 'Ingelogd via',
97    },
98  };
99
100  function t(key) {
101    var pack = STR[lang()] || STR.en;
102    return pack[key] || STR.en[key] || key;
103  }
104
105  function qs(sel, root) {
106    return (root || document).querySelector(sel);
107  }
108
109  function escapeHtml(s) {
110    return String(s)
111      .replace(/&/g, '&')
112      .replace(/</g, '&lt;')
113      .replace(/>/g, '&gt;')
114      .replace(/"/g, '&quot;');
115  }
116
117  function formatTime(ts) {
118    try {
119      return new Date(ts * 1000).toLocaleString(lang(), {
120        dateStyle: 'medium',
121        timeStyle: 'short',
122      });
123    } catch (e) {
124      return '';
125    }
126  }
127
128  function apiGet(path) {
129    return fetch(API + path, { credentials: 'same-origin' }).then(function (r) {
130      return r.text().then(function (txt) {
131        var j = {};
132        try {
133          j = txt ? JSON.parse(txt) : {};
134        } catch (e) {
135          j = {};
136        }
137        if (!r.ok) throw new Error(j.error || txt.slice(0, 120) || 'HTTP ' + r.status);
138        return j;
139      });
140    });
141  }
142
143  function apiPost(path, body, csrf) {
144    return fetch(API + path, {
145      method: 'POST',
146      credentials: 'same-origin',
147      headers: {
148        'Content-Type': 'application/json',
149        'X-CSRF-Token': csrf,
150      },
151      body: JSON.stringify(body),
152    }).then(function (r) {
153      return r.text().then(function (txt) {
154        var j = {};
155        try {
156          j = txt ? JSON.parse(txt) : {};
157        } catch (e) {
158          j = {};
159        }
160        if (!r.ok) throw new Error(j.error || txt.slice(0, 120) || 'HTTP ' + r.status);
161        return j;
162      });
163    });
164  }
165
166  var state = {
167    user: null,
168    moderator: false,
169    csrf: '',
170    parentId: null,
171    providersEnabled: {},
172    activeProvider: null,
173  };
174
175  function setFlash(kind, msg) {
176    var el = qs('#comments-flash');
177    if (!el) return;
178    el.className = 'guide-comments-flash guide-comments-flash--' + kind;
179    el.textContent = msg;
180    el.hidden = false;
181    if (kind === 'ok') {
182      setTimeout(function () {
183        el.hidden = true;
184      }, 6000);
185    }
186  }
187
188  function consumeAuthQuery() {
189    var u = new URL(window.location.href);
190    var a = u.searchParams.get('auth');
191    if (a === 'ok') {
192      setFlash('ok', t('authOk'));
193      u.searchParams.delete('auth');
194      history.replaceState({}, '', u.pathname + u.search + u.hash);
195    } else if (a && /^err_/.test(a)) {
196      setFlash('err', t('authErr'));
197      u.searchParams.delete('auth');
198      history.replaceState({}, '', u.pathname + u.search + u.hash);
199    }
200  }
201
202  function isAllowedAvatarUrl(url) {
203    if (!url || typeof url !== 'string') return false;
204    try {
205      var u = new URL(url, window.location.origin);
206      if (u.protocol !== 'https:') return false;
207      var host = u.hostname.toLowerCase();
208      if (host === 'cdn.discordapp.com') {
209        var p = u.pathname;
210        return p.indexOf('/avatars/') === 0 || p.indexOf('/embed/avatars/') === 0;
211      }
212      if (host.indexOf('googleusercontent.com') !== -1) {
213        return true;
214      }
215      return false;
216    } catch (e) {
217      return false;
218    }
219  }
220
221  function safeAvatarUrl(url) {
222    return isAllowedAvatarUrl(url)
223      ? url
224      : 'https://cdn.discordapp.com/embed/avatars/0.png';
225  }
226
227  function isSafeCommentLink(url) {
228    try {
229      var u = new URL(url, window.location.origin);
230      return u.protocol === 'https:' || u.protocol === 'http:';
231    } catch (e) {
232      return false;
233    }
234  }
235
236  function appendSanitizedNode(source, target) {
237    if (source.nodeType === Node.TEXT_NODE) {
238      target.appendChild(document.createTextNode(source.textContent || ''));
239      return;
240    }
241    if (source.nodeType !== Node.ELEMENT_NODE) return;
242
243    var tag = source.tagName.toLowerCase();
244    var allowed = tag === 'p' || tag === 'br' || tag === 'strong' || tag === 'em' || tag === 'code' || tag === 'a';
245    if (!allowed) {
246      for (var child = source.firstChild; child; child = child.nextSibling) {
247        appendSanitizedNode(child, target);
248      }
249      return;
250    }
251
252    var el = document.createElement(tag);
253    if (tag === 'a') {
254      var href = source.getAttribute('href') || '';
255      if (!isSafeCommentLink(href)) {
256        for (var linkChild = source.firstChild; linkChild; linkChild = linkChild.nextSibling) {
257          appendSanitizedNode(linkChild, target);
258        }
259        return;
260      }
261      el.href = href;
262      el.target = '_blank';
263      el.rel = 'nofollow ugc noopener noreferrer';
264    }
265    for (var nested = source.firstChild; nested; nested = nested.nextSibling) {
266      appendSanitizedNode(nested, el);
267    }
268    target.appendChild(el);
269  }
270
271  function setCommentHtml(host, html) {
272    var template = document.createElement('template');
273    template.innerHTML = String(html || '');
274    while (host.firstChild) host.removeChild(host.firstChild);
275    for (var child = template.content.firstChild; child; child = child.nextSibling) {
276      appendSanitizedNode(child, host);
277    }
278  }
279
280  function safeTimestamp(value) {
281    var ts = Number(value);
282    return Number.isFinite(ts) && ts > 0 ? ts : 0;
283  }
284
285  function oauthNextPath() {
286    var next = window.location.pathname || '/pintrading/';
287    if (!next.startsWith('/pintrading')) next = '/pintrading/';
288    if (next === '/pintrading') next = '/pintrading/';
289    return next;
290  }
291
292  function authStartHref(provider) {
293    return API + '/auth/' + provider + '?next=' + encodeURIComponent(oauthNextPath());
294  }
295
296  function providerLabel(name) {
297    var labels = { discord: 'Discord', google: 'Google' };
298    return labels[name] || name;
299  }
300
301  function renderAuth() {
302    var host = qs('#comments-auth');
303    if (!host) return;
304    host.innerHTML = '';
305
306    if (state.user) {
307      var row = document.createElement('div');
308      row.className = 'guide-comments-user';
309      var img = document.createElement('img');
310      img.src = safeAvatarUrl(state.user.avatar_url);
311      img.alt = '';
312      img.width = 40;
313      img.height = 40;
314      img.loading = 'lazy';
315      var meta = document.createElement('div');
316      meta.className = 'guide-comments-user-meta';
317      var name = document.createElement('span');
318      name.textContent = state.user.username;
319      meta.appendChild(name);
320      if (state.activeProvider) {
321        var via = document.createElement('span');
322        via.className = 'guide-comments-user-via';
323        via.textContent = t('signedInVia') + ' ' + providerLabel(state.activeProvider);
324        meta.appendChild(via);
325      }
326      row.appendChild(img);
327      row.appendChild(meta);
328      var out = document.createElement('button');
329      out.type = 'button';
330      out.className = 'guide-comments-btn guide-comments-btn--ghost';
331      out.textContent = t('logout');
332      out.addEventListener('click', function () {
333        out.disabled = true;
334        apiPost('/logout', { csrf: state.csrf }, state.csrf)
335          .then(function () {
336            state.user = null;
337            state.moderator = false;
338            state.activeProvider = null;
339            return refreshSession();
340          })
341          .then(function () {
342            renderAuth();
343            renderCompose();
344            if (!state.moderator) renderMod([]);
345          })
346          .catch(function () {
347            setFlash('err', t('errorGeneric'));
348          })
349          .finally(function () {
350            out.disabled = false;
351          });
352      });
353      host.appendChild(row);
354      host.appendChild(out);
355      if (!state.moderator) {
356        var prov = (state.user && state.user.providers) || [];
357        var hasDiscord = prov.indexOf('discord') !== -1;
358        var mh = document.createElement('p');
359        mh.className = 'guide-comments-hint guide-comments-hint--full';
360        mh.textContent = hasDiscord ? t('modHintDiscordApi') : t('modHintGoogleOnly');
361        host.appendChild(mh);
362      }
363      return;
364    }
365
366    var pe = state.providersEnabled || {};
367    var anyProvider = pe.discord || pe.google;
368    if (!anyProvider) {
369      var warn = document.createElement('p');
370      warn.className = 'guide-comments-hint';
371      warn.textContent = t('pendingNote');
372      host.appendChild(warn);
373      return;
374    }
375
376    var actions = document.createElement('div');
377    actions.className = 'guide-comments-auth-actions';
378
379    function addOAuthBtn(provider, labelKey) {
380      var a = document.createElement('a');
381      a.className = 'guide-comments-btn guide-comments-btn--' + provider;
382      a.href = authStartHref(provider === 'discord' ? 'login' : provider);
383      a.textContent = t(labelKey);
384      a.setAttribute('aria-label', t(labelKey));
385      actions.appendChild(a);
386    }
387
388    if (pe.discord) addOAuthBtn('discord', 'loginDiscord');
389    if (pe.google) addOAuthBtn('google', 'loginGoogle');
390
391    host.appendChild(actions);
392
393    var note = document.createElement('span');
394    note.className = 'guide-comments-hint';
395    note.style.margin = '0';
396    note.textContent = t('pendingNote');
397    host.appendChild(note);
398  }
399
400  function renderCompose() {
401    var box = qs('#comments-compose');
402    if (!box) return;
403    if (!state.user) {
404      box.innerHTML = '<p class="guide-comments-hint">' + escapeHtml(t('pendingNote')) + '</p>';
405      return;
406    }
407    box.innerHTML =
408      '<input type="text" id="comments-hp" class="guide-comments-hp" tabindex="-1" autocomplete="off" aria-hidden="true">' +
409      '<div id="comments-reply-hint" class="guide-comments-replying" hidden></div>' +
410      '<label for="comments-body">' +
411      escapeHtml(t('placeholder')) +
412      '</label>' +
413      '<textarea id="comments-body" maxlength="4000" rows="5"></textarea>' +
414      '<p class="guide-comments-hint">' +
415      escapeHtml(t('hint')) +
416      '</p>' +
417      '<div class="guide-comments-compose-actions">' +
418      '<button type="button" class="guide-comments-btn" id="comments-submit">' +
419      escapeHtml(t('submit')) +
420      '</button>' +
421      '<button type="button" class="guide-comments-btn guide-comments-btn--ghost" id="comments-cancel-reply" hidden>' +
422      escapeHtml(t('cancelReply')) +
423      '</button></div>' +
424      '<p class="guide-comments-status" id="comments-compose-status" hidden></p>';
425
426    var ta = qs('#comments-body', box);
427    var submit = qs('#comments-submit', box);
428    var cancel = qs('#comments-cancel-reply', box);
429    var rh = qs('#comments-reply-hint', box);
430    var st = qs('#comments-compose-status', box);
431
432    function updateReplyUi() {
433      if (state.parentId) {
434        rh.hidden = false;
435        rh.textContent = t('replyingTo') + ' (#' + state.parentId + ')';
436        cancel.hidden = false;
437      } else {
438        rh.hidden = true;
439        cancel.hidden = true;
440      }
441    }
442    updateReplyUi();
443
444    cancel.addEventListener('click', function () {
445      state.parentId = null;
446      updateReplyUi();
447    });
448
449    submit.addEventListener('click', function () {
450      var hpEl = qs('#comments-hp', box);
451      if (hpEl && hpEl.value) {
452        return;
453      }
454      var body = ((ta && ta.value) || '').trim();
455      if (!body) return;
456      submit.disabled = true;
457      st.hidden = true;
458      apiPost(
459        '/comments',
460        { body: body, parent_id: state.parentId, csrf: state.csrf, hp: hpEl ? hpEl.value : '' },
461        state.csrf
462      )
463        .then(function () {
464          ta.value = '';
465          state.parentId = null;
466          updateReplyUi();
467          st.textContent = t('submitted');
468          st.hidden = false;
469          return loadComments();
470        })
471        .then(function () {
472          if (state.moderator) return loadMod();
473        })
474        .catch(function (e) {
475          setFlash('err', e.message || t('errorGeneric'));
476        })
477        .finally(function () {
478          submit.disabled = false;
479        });
480    });
481  }
482
483  function renderComment(node) {
484    var author = (node && node.author) || {};
485    var createdAt = safeTimestamp(node && node.created_at);
486    var li = document.createElement('li');
487    li.className = 'guide-comment';
488    var card = document.createElement('article');
489    card.className = 'guide-comment-card';
490    card.setAttribute('itemscope', '');
491    card.setAttribute('itemtype', 'https://schema.org/Comment');
492
493    var img = document.createElement('img');
494    img.src = safeAvatarUrl(author.avatar_url);
495    img.alt = '';
496    img.width = 44;
497    img.height = 44;
498    img.loading = 'lazy';
499
500    var body = document.createElement('div');
501    body.className = 'guide-comment-body';
502
503    var meta = document.createElement('div');
504    meta.className = 'guide-comment-meta';
505    var strong = document.createElement('strong');
506    strong.setAttribute('itemprop', 'author');
507    strong.textContent = author.username || 'User';
508    var time = document.createElement('time');
509    if (createdAt) time.dateTime = new Date(createdAt * 1000).toISOString();
510    time.textContent = createdAt ? formatTime(createdAt) : '';
511    meta.appendChild(strong);
512    meta.appendChild(time);
513
514    var text = document.createElement('div');
515    text.className = 'guide-comment-text';
516    text.setAttribute('itemprop', 'text');
517    setCommentHtml(text, node && node.body_html);
518
519    var actions = document.createElement('div');
520    actions.className = 'guide-comment-actions';
521    if (state.user) {
522      var rb = document.createElement('button');
523      rb.type = 'button';
524      rb.textContent = t('reply');
525      rb.addEventListener('click', function () {
526        state.parentId = node.id;
527        renderCompose();
528        var ta2 = qs('#comments-body');
529        if (ta2) ta2.focus();
530      });
531      actions.appendChild(rb);
532    }
533
534    if (state.moderator) {
535      var rm = document.createElement('button');
536      rm.type = 'button';
537      rm.className = 'guide-comment-remove';
538      rm.textContent = t('removePublic');
539      rm.addEventListener('click', function () {
540        if (!window.confirm(t('removePublicConfirm'))) return;
541        rm.disabled = true;
542        apiPost('/moderation/remove', { id: node.id, csrf: state.csrf }, state.csrf)
543          .then(function () {
544            return Promise.all([loadComments(), loadMod()]);
545          })
546          .catch(function (e) {
547            setFlash('err', e.message || t('errorGeneric'));
548          })
549          .finally(function () {
550            rm.disabled = false;
551          });
552      });
553      actions.appendChild(rm);
554    }
555
556    body.appendChild(meta);
557    body.appendChild(text);
558    body.appendChild(actions);
559    card.appendChild(img);
560    card.appendChild(body);
561    li.appendChild(card);
562
563    if (node.children && node.children.length) {
564      var ul = document.createElement('ul');
565      ul.className = 'guide-comments-list';
566      for (var i = 0; i < node.children.length; i++) {
567        ul.appendChild(renderComment(node.children[i]));
568      }
569      li.appendChild(ul);
570    }
571    return li;
572  }
573
574  function renderList(tree) {
575    var host = qs('#comments-list');
576    if (!host) return;
577    host.innerHTML = '';
578    if (!tree || !tree.length) {
579      var p = document.createElement('p');
580      p.className = 'guide-comments-hint';
581      p.textContent = t('none');
582      host.appendChild(p);
583      return;
584    }
585    var ul = document.createElement('ul');
586    ul.className = 'guide-comments-list';
587    for (var j = 0; j < tree.length; j++) {
588      ul.appendChild(renderComment(tree[j]));
589    }
590    host.appendChild(ul);
591  }
592
593  function renderMod(pending) {
594    var host = qs('#comments-mod');
595    if (!host) return;
596    if (!state.moderator) {
597      host.innerHTML = '';
598      host.hidden = true;
599      return;
600    }
601    host.hidden = false;
602    host.innerHTML = '<h3>' + escapeHtml(t('modTitle')) + '</h3><div id="comments-mod-inner"></div>';
603    var inner = qs('#comments-mod-inner', host);
604    if (!pending || !pending.length) {
605      inner.innerHTML = '<p class="guide-comments-mod-empty">' + escapeHtml(t('modEmpty')) + '</p>';
606      return;
607    }
608    for (var i = 0; i < pending.length; i++) {
609      inner.appendChild(renderModItem(pending[i]));
610    }
611  }
612
613  function renderModItem(item) {
614    var author = (item && item.author) || {};
615    var createdAt = safeTimestamp(item && item.created_at);
616    var wrap = document.createElement('div');
617    wrap.className = 'guide-comments-mod-item';
618    var head = document.createElement('div');
619    head.className = 'guide-comments-mod-head';
620    var img = document.createElement('img');
621    img.src = safeAvatarUrl(author.avatar_url);
622    img.alt = '';
623    var who = document.createElement('strong');
624    who.textContent = author.username || 'User';
625    var meta = document.createElement('span');
626    meta.style.fontSize = '12px';
627    meta.style.color = 'var(--text-muted)';
628    var providerHint = '';
629    if (author.providers && author.providers.length) {
630      providerHint = ' · ' + author.providers.join(', ');
631    }
632    meta.textContent = (createdAt ? formatTime(createdAt) : '') + ' · #' + item.id + providerHint;
633    head.appendChild(img);
634    head.appendChild(who);
635    head.appendChild(meta);
636
637    var prev = document.createElement('div');
638    prev.className = 'guide-comments-mod-preview';
639    setCommentHtml(prev, item && item.body_html);
640
641    var actions = document.createElement('div');
642    actions.className = 'guide-comments-mod-actions';
643    var ap = document.createElement('button');
644    ap.type = 'button';
645    ap.className = 'approve';
646    ap.textContent = t('approve');
647    var rj = document.createElement('button');
648    rj.type = 'button';
649    rj.className = 'reject';
650    rj.textContent = t('reject');
651
652    function act(status) {
653      ap.disabled = rj.disabled = true;
654      apiPost('/moderation/' + status, { id: item.id, csrf: state.csrf }, state.csrf)
655        .then(function () {
656          wrap.remove();
657          return Promise.all([loadComments(), loadMod()]);
658        })
659        .catch(function (e) {
660          setFlash('err', e.message || t('errorGeneric'));
661        })
662        .finally(function () {
663          ap.disabled = rj.disabled = false;
664        });
665    }
666    ap.addEventListener('click', function () {
667      act('approve');
668    });
669    rj.addEventListener('click', function () {
670      act('reject');
671    });
672    actions.appendChild(ap);
673    actions.appendChild(rj);
674
675    wrap.appendChild(head);
676    wrap.appendChild(prev);
677    wrap.appendChild(actions);
678    return wrap;
679  }
680
681  function refreshSession() {
682    return apiGet('/session').then(function (d) {
683      state.user = d.user;
684      state.moderator = !!d.moderator;
685      state.csrf = d.csrf || '';
686      state.providersEnabled = d.providers_enabled || {};
687      state.activeProvider = d.active_provider || null;
688      return d;
689    });
690  }
691
692  function loadComments() {
693    return apiGet('/comments').then(function (d) {
694      renderList(d.comments || []);
695    });
696  }
697
698  function loadMod() {
699    if (!state.moderator) {
700      renderMod([]);
701      return Promise.resolve();
702    }
703    return apiGet('/moderation/pending').then(function (d) {
704      renderMod(d.pending || []);
705    });
706  }
707
708  function init() {
709    var root = qs('#comments');
710    if (!root) return;
711
712    consumeAuthQuery();
713
714    refreshSession()
715      .then(function () {
716        renderAuth();
717        renderCompose();
718        return loadComments();
719      })
720      .then(function () {
721        return loadMod();
722      })
723      .catch(function () {
724        setFlash('err', t('errorGeneric'));
725      });
726  }
727
728  if (document.readyState === 'loading') {
729    document.addEventListener('DOMContentLoaded', init);
730  } else {
731    init();
732  }
733})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.