Private/ReportScript.ps1
|
# What the Report does in a browser: its script, carried inside the page. See Private/Report.ps1. # # Comfort only. The Report is complete without it: <details> folds by itself. $script:ReportScript = @' <script> (function(){ function each(sel, fn){ Array.prototype.forEach.call(document.querySelectorAll(sel), fn); } // One button opens and folds all of a part: everything, where anything shown is folded, // and nothing, where all of it is open. Its sign says which a click will do, and is said // again whenever something opens or folds - by hand, by a view, by a search. var foldSigns = { open: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 6l5 5 5-5M7 13l5 5 5-5"/></svg>', shut: '<svg viewBox="0 0 24 24" width="15" height="15" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M7 18l5-5 5 5M7 11l5-5 5 5"/></svg>' }; function shownOnly(list){ return Array.prototype.filter.call(list, function(x){ return !x.hidden && !x.classList.contains('sx') && x.style.display !== 'none' && x.offsetParent !== null; }); } function foldItems(b){ if (b.classList.contains('gall')) return shownOnly(b.closest('details.group').querySelectorAll('.row.more')); if (b.hasAttribute('data-cards')) return shownOnly(document.querySelectorAll('#ch-action .card.more')); // The folds, and not the help inside one: that is opened by whoever wants to read it. return shownOnly(document.querySelectorAll(b.getAttribute('data-fold') + ' details:not(.help)')); } function isOpen(x){ return x.tagName === 'DETAILS' ? x.open : x.classList.contains('open'); } function syncFold(){ each('[data-fold],[data-cards],.gall', function(b){ var g = b.classList.contains('gall') ? b.closest('details.group') : null; var items = foldItems(b); var open = (g && !g.open) || !items.length || items.some(function(x){ return !isOpen(x); }); b.innerHTML = open ? foldSigns.open : foldSigns.shut; b.title = b.getAttribute(open ? 'data-open' : 'data-shut') || ''; b.setAttribute('aria-label', b.title); }); } var syncing = false; function syncSoon(){ if (syncing) return; syncing = true; setTimeout(function(){ syncing = false; syncFold(); }, 0); } document.addEventListener('toggle', syncSoon, true); document.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('[data-fold],[data-cards],.gall') : null; if (!b) return; // The button of an area sits in its head: a click on it opens the area, and folds nothing. var g = b.classList.contains('gall') ? b.closest('details.group') : null, forced = false; if (g) { e.preventDefault(); e.stopPropagation(); if (!g.open) { g.open = true; forced = true; markMore(); } } var items = foldItems(b), open = forced || items.some(function(x){ return !isOpen(x); }); items.forEach(function(x){ if (x.tagName === 'DETAILS') x.open = open; else x.classList.toggle('open', open); }); syncFold(); }, true); var q = document.getElementById('evidence-filter'); if (q) q.addEventListener('input', function(){ var words = q.value.toLowerCase().split(/\s+/).filter(Boolean); each('#evidence details:not(.help)', function(d){ var t = d.getAttribute('data-title') || ''; d.style.display = words.every(function(w){ return t.indexOf(w) >= 0; }) ? '' : 'none'; }); // A heading with nothing left under it goes too. each('#evidence .kind', function(k){ var left = Array.prototype.some.call(k.querySelectorAll('details:not(.help)'), function(d){ return d.style.display !== 'none'; }); k.style.display = left ? '' : 'none'; }); each('#evidence .egroup', function(g){ var left = Array.prototype.some.call(g.querySelectorAll('.kind'), function(k){ return k.style.display !== 'none'; }); g.style.display = left ? '' : 'none'; }); }); // The tree beside the Report. Picking an entry shows that part of the Report and // nothing else; a Finding is one line until somebody opens it. Without this script // every chapter stands under the other and everything is open. var root = document.documentElement, nav = document.getElementById('nav'), view = 'overview'; // Which Findings have more than their one line: further lines, what it means, what to // do, where the evidence is - or a first line too long for the width, cut off with // "...". Only those carry the sign and open on a click: a line that opens to the same // line was a click that did nothing. Decided where they are shown, as the width there // decides what is cut off. function markMore(){ each('#ch-action .card,#checks .row', function(el){ if (el.classList.contains('open') || el.offsetParent === null) return; var card = el.classList.contains('card'); var more = !!el.querySelector(card ? '.parts>li:nth-child(2),.meaning,.todo,.refs' : '.val>div:nth-child(2),.meaning,.hint,.refs'); var first = el.querySelector(card ? '.parts>li:first-child' : '.val>div:first-child'); if (!more && first && first.scrollWidth > first.clientWidth + 1) more = true; el.classList.toggle('more', more); }); } window.addEventListener('resize', function(){ markMore(); }); // Ends a search; the search below says how. A link ends it. var stopFind = function(){}; // A pick while something is searched for searches there; the search below says how. var findIn = function(){ return false; }; // Told of every view shown, so that the search can say where it would look. var viewShown = function(){}; function chapter(name){ return document.querySelector('[data-chapter="' + name + '"]'); } // A view in the address: "#v:cat:Network". A Finding's own address is its id, "#r-12", // and stays what it is. function viewOfHash(){ var h = location.hash.slice(1); try { h = decodeURIComponent(h); } catch (x) { } return h.indexOf('v:') === 0 ? h.slice(2) : ''; } // A view picked in the tree is a step of the browser's history; the same view again is not. // Not "go": the search has a go of its own, declared in a block, and it took this one's place. function goView(next){ closeFull(); show(next); try { if (!(history.state && history.state.view === next)) history.pushState({ view: next }, '', '#v:' + encodeURIComponent(next)); } catch (x) { } } // What the head of an area counts is what stands under it: picked for one application, // "Apps" with the counts of every application was read as that application's. var levels = ['FAIL', 'WARN', 'INFO', 'OK']; function tally(g, rows){ var s = g.querySelector('summary'), b = s ? s.querySelector('b') : null; if (!b) return; if (!g.whole) g.whole = { cls: s.className, html: Array.prototype.map.call(s.querySelectorAll('.badge'), function(x){ return x.outerHTML; }).join(' ') }; var cls = g.whole.cls, html = g.whole.html; if (rows) { var n = {}; rows.forEach(function(r){ levels.forEach(function(v){ if (r.classList.contains(v)) n[v] = (n[v] || 0) + 1; }); }); var seen = levels.filter(function(v){ return n[v]; }); html = seen.map(function(v){ return '<span class="badge ' + v + '">' + n[v] + ' ' + v + '</span>'; }).join(' '); if (seen.length) cls = seen[0]; } Array.prototype.forEach.call(s.querySelectorAll('.badge'), function(x){ x.parentNode.removeChild(x); }); s.className = cls; (s.querySelector('.gall') || b).insertAdjacentHTML('afterend', html); } function show(next){ if (!nav) return; view = next; document.body.setAttribute('data-view', next); var isCat = next.indexOf('cat:') === 0, isApp = next.indexOf('app:') === 0, isFind = next === 'find'; var narrowed = next === 'info' || isCat || isApp || isFind; // What machine this is and how the Run went is a chapter, and no head over every // other: it stood above whatever was picked, and pushed that out of sight. What puts // the Report itself in doubt is said with the overview and with what needs doing. var want = { overview: next === 'overview', alerts: next === 'overview' || next === 'action', leads: next === 'action', action: next === 'action', checks: next === 'all' || narrowed, evidence: next === 'evidence' || isFind, glossary: next === 'glossary' || isFind, network: next === 'network' }; for (var k in want) { var c = chapter(k); if (c) c.hidden = !want[k]; } var key = next.slice(4); each('#checks details.group', function(g){ var any = false, head = null, under = false, kept = []; Array.prototype.forEach.call(g.children, function(el){ if (el.classList.contains('subject')) { if (head) head.hidden = !under; head = el; under = false; return; } if (!el.classList.contains('row')) return; var app = el.getAttribute('data-app') || ''; var keep = !narrowed || (next === 'info' && el.classList.contains('INFO')) || (isCat && g.getAttribute('data-cat') === key && !app) || (isApp && app === key) || (isFind && !el.classList.contains('sx')); el.hidden = !keep; if (keep) { any = true; under = true; kept.push(el); } }); if (head) head.hidden = !under; g.hidden = !any; tally(g, narrowed ? kept : null); // One area or one application is what was asked for, and stands open. The hints and // all Checks are many areas: each folded, to be opened by whoever wants one. if (want.checks) g.open = (isCat || isApp || isFind) && any; }); var note = document.getElementById('viewnote'); if (note) { note.hidden = !narrowed || isFind; var picked = nav.querySelector('[data-pick="' + next.replace(/"/g, '\\"') + '"] .nm'); note.querySelector('b').textContent = picked ? picked.textContent : ''; } each('#nav [data-pick]', function(b){ b.setAttribute('aria-current', String(b.getAttribute('data-pick') === next)); }); viewShown(next); markMore(); syncFold(); } if (nav) { root.classList.add('js'); nav.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('a,button') : null; if (!b) return; if (b.hasAttribute('data-pick')) { e.preventDefault(); if (!findIn(b.getAttribute('data-pick'))) goView(b.getAttribute('data-pick')); window.scrollTo(0, 0); return; } if (b.classList.contains('gh')) { b.setAttribute('aria-expanded', String(b.getAttribute('aria-expanded') === 'false')); return; } if (b.classList.contains('more')) { var g = b.parentNode, all = g.classList.toggle('all'); b.textContent = all ? nav.getAttribute('data-less') : nav.getAttribute('data-more').replace('{0}', b.getAttribute('data-count')); } }); var nq = document.getElementById('nav-filter'); if (nq) nq.addEventListener('input', function(){ var word = nq.value.toLowerCase().trim(); each('#nav .navgroup', function(g){ g.classList.toggle('searching', !!word); Array.prototype.forEach.call(g.querySelectorAll('[data-pick]'), function(b){ b.hidden = !!word && b.textContent.toLowerCase().indexOf(word) < 0; }); }); }); var back = document.querySelector('#viewnote button'); if (back) back.addEventListener('click', function(){ goView('all'); }); // A Finding opens where it is clicked - but not by marking text in it, and not by a // link or a button inside it. document.addEventListener('click', function(e){ if (!e.target.closest || e.target.closest('a,button,input,summary,.long')) return; if (window.getSelection && window.getSelection().toString()) return; // Anywhere in it: a line with one small place to hit is a line that is missed. var item = e.target.closest('.card') || e.target.closest('#checks .row'); if (!item) return; // The second click of a double-click marks a word, and is no click to fold. if (e.detail > 1) return; // An open one closes from its head only: a click in its text is a click to read it, // and a double-click to mark a word closed it under the pointer. One with nothing // more does not open. if (item.classList.contains('open') && !e.target.closest('.card>header,#checks .row .what')) return; if (!item.classList.contains('open') && !item.classList.contains('more')) return; item.classList.toggle('open'); syncFold(); }); // A Report opens on what machine this is and how the Run went: from there the tree // leads to what stands out, and the reader knows what it is about. Opened again with // a view in its address - reloaded, or sent as a link - it opens on that view. var start = viewOfHash(); show(start || 'overview'); try { history.replaceState({ view: start || 'overview' }, ''); } catch (x) { } // Every line of one area opened, or folded, from its head, with the words of the // button over all of them. var all = document.querySelector('[data-fold="#checks"]'); if (all) { each('#checks details.group>summary', function(s){ var b = s.querySelector('b'); if (!b) return; var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'gall ic'; btn.setAttribute('data-open', all.getAttribute('data-open') || ''); btn.setAttribute('data-shut', all.getAttribute('data-shut') || ''); b.insertAdjacentElement('afterend', btn); }); } syncFold(); } // A Hint that names a piece of evidence leads there: shown again if filtered away, and open. function reveal(){ var d = location.hash.length > 1 ? document.getElementById(location.hash.slice(1)) : null; if (!d) return; // Out of a table over the whole window: the window is the Report's again. closeFull(); stopFind(); // In whatever chapter it is: that chapter is shown, and the Finding opened. var home = d.closest ? d.closest('[data-chapter]') : null; if (nav && home) { var name = home.getAttribute('data-chapter'); show(name === 'checks' ? 'all' : (name === 'evidence' || name === 'overview' || name === 'glossary' || name === 'network' ? name : 'action')); if (d.classList.contains('card') || d.classList.contains('row')) d.classList.add('open'); } // A Section is a fold of its own; a Finding's line sits in the fold of its Category. var fold = d.tagName === 'DETAILS' ? d : (d.closest ? d.closest('details') : null); if (fold) { fold.style.display = ''; if (fold.parentNode && fold.parentNode.style) fold.parentNode.style.display = ''; fold.open = true; } // Below the search, which stands over the top of the page, and outlined for a moment: // at the very top it was hidden behind the search, and among its neighbours not found. d.scrollIntoView(); d.classList.remove('jumped'); void d.offsetWidth; d.classList.add('jumped'); // Taken off once it has run: a part shown again starts its animations again, and every // entry ever jumped to lit up once more. d.addEventListener('animationend', function done(){ d.classList.remove('jumped'); d.removeEventListener('animationend', done); }); } window.addEventListener('hashchange', reveal); // Back and forward go from view to view as the tree was used, and from a Finding a link // led to back to where the link was. window.addEventListener('popstate', function(e){ if (!nav) return; var to = (e.state && e.state.view) || viewOfHash(); if (to) { stopFind(); show(to); } }); document.addEventListener('click', function(e){ if (e.target.closest && e.target.closest('a.jump')) setTimeout(reveal, 0); }); reveal(); document.addEventListener('click', function(e){ var el = e.target.closest ? e.target.closest('.long') : null; if (el && !window.getSelection().toString()) el.classList.toggle('open'); }); // A Finding as text, for a ticket or a mail. The clipboard of a page opened from a // file is not everywhere to be had, so there is the old way behind the new one. function put(text, done){ function old(){ var t = document.createElement('textarea'); t.value = text; t.style.position = 'fixed'; t.style.opacity = '0'; document.body.appendChild(t); t.select(); try { if (document.execCommand('copy')) done(); } catch (x) {} document.body.removeChild(t); } if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(text).then(done, old); else old(); } // A table of the evidence as it is shown - filtered, searched - with its title and its // headings, a tab between the cells: pasted into Excel it is a table, into a ticket it // is lines. Text that is no table is copied as it stands. function tableText(b){ var d = b.closest('details:not(.help)'), head = d ? d.querySelector('summary b') : null; var lines = [head ? head.textContent.trim() : '']; var t = d ? d.querySelector('table.data') : null; var cell = function(c){ return c.textContent.replace(/\s+/g, ' ').trim(); }; if (t) { lines.push(Array.prototype.map.call(t.tHead.rows[0].cells, cell).join('\t')); Array.prototype.forEach.call(t.tBodies[0].rows, function(r){ if (r.classList.contains('grp') || r.classList.contains('sx') || r.style.display === 'none') return; lines.push(Array.prototype.map.call(r.cells, cell).join('\t')); }); } else { var pre = d ? d.querySelector('.evi pre') : null; if (pre) lines.push(pre.textContent); } return lines.join('\n'); } var doneSign = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12.5l4.5 4.5L19 7.5"/></svg>'; document.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('button.copy') : null; if (!b) return; // In the head of a fold, and no click on the fold. e.preventDefault(); e.stopPropagation(); var sign = b.innerHTML, title = b.title; put(b.classList.contains('tcopy') ? tableText(b) : (b.getAttribute('data-copy') || ''), function(){ b.innerHTML = doneSign; b.title = b.getAttribute('data-done') || title; b.classList.add('done'); setTimeout(function(){ b.innerHTML = sign; b.title = title; b.classList.remove('done'); }, 1500); }); }); var copySign = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"/><path d="M5 15V6a2 2 0 0 1 2-2h9"/></svg>'; var fullIcon = { open: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5"/></svg>', shut: '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5"/></svg>' }; // What a search or a filter of the Report understands. Words are all wanted; ODER or | // between two is either; -word, !word or NICHT word is not wanted; ( ) groups; "in // quotes" is one; Spalte:Wert looks in that column of a table only (Pruefung:, // Anwendung:, Bereich: on a Finding); FAIL WARN INFO OK in capitals is a state. The // words of the language are in capitals, so that "oder" is still a word to find. Case // and umlauts do not count: "domane" finds "Domaene" as written with one. var Q = (function(){ // Folded letter for letter, so that a place in the folded text is the same place in the text. function fold(s){ return (s || '').toLowerCase().replace(/[\u00e0-\u00e5]/g, 'a').replace(/[\u00f2-\u00f6]/g, 'o').replace(/[\u00f9-\u00fc]/g, 'u') .replace(/[\u00e8-\u00eb]/g, 'e').replace(/\u00df/g, 's').replace(/\u00a0/g, ' '); } function wrap(neg, node){ return neg ? { t: 'not', a: node } : node; } function tokens(v){ var out = [], i = 0, n = v.length; while (i < n) { var c = v.charAt(i); if (/\s/.test(c)) { i++; continue; } if (c === '(' || c === ')' || c === '|') { out.push({ k: c }); i++; continue; } var neg = false; if ((c === '-' || c === '!') && i + 1 < n && !/\s/.test(v.charAt(i + 1))) { neg = true; i++; if (v.charAt(i) === '(') { out.push({ k: 'not' }); continue; } } // A column: a name with a letter in it, of two letters or more - "C:\" is a drive, // a time of day such as 13:35, "https:" an address, "fe80::1" an address. var field = '', m = /^([^\s"():|]+):(?=[^\s:])/.exec(v.slice(i)); if (m && m[1].length > 1 && /[A-Za-z\u00c0-\u024f]/.test(m[1]) && !/^(https?|ftp|file|ldaps?|smb)$/i.test(m[1])) { field = m[1]; i += m[0].length; } var word = '', quoted = false; if (v.charAt(i) === '"') { var e = v.indexOf('"', i + 1); if (e < 0) e = n; word = v.slice(i + 1, e); i = e + 1; quoted = true; } else { var s = i; while (i < n && !/[\s()|]/.test(v.charAt(i))) i++; word = v.slice(s, i); } if (!quoted && !field && !neg) { if (word === 'ODER' || word === 'OR') { out.push({ k: '|' }); continue; } if (word === 'UND' || word === 'AND' || word === '&') continue; if (word === 'NICHT' || word === 'NOT') { out.push({ k: 'not' }); continue; } } if (!quoted && !field && levels.indexOf(word) >= 0) { out.push({ k: 't', node: wrap(neg, { t: 'level', l: word }) }); continue; } word = fold(word); if (!word.trim()) continue; out.push({ k: 't', node: wrap(neg, field ? { t: 'field', f: fold(field), w: word } : { t: 'word', w: word }) }); } return out; } function parse(v){ var t = tokens(v || ''), p = 0; function one(list, kind){ list = list.filter(Boolean); return list.length > 1 ? { t: kind, a: list } : (list[0] || null); } function either(){ var a = [all()]; while (p < t.length && t[p].k === '|') { p++; a.push(all()); } return one(a, 'or'); } function all(){ var a = []; while (p < t.length && t[p].k !== '|' && t[p].k !== ')') a.push(unary()); return one(a, 'and'); } function unary(){ var k = t[p]; if (!k) return null; p++; if (k.k === 'not') { var x = unary(); return x ? { t: 'not', a: x } : null; } if (k.k === '(') { var e = either(); if (t[p] && t[p].k === ')') p++; return e; } return k.node || null; } var root = either(); // A ")" too many is passed over, and what follows it still counts. while (p < t.length) { p++; var more = either(); if (more) root = root ? { t: 'and', a: [root, more] } : more; } return root; } // Whether an item is wanted. An item says what it is as text, and may say whether it // is in a state and what is in a column of it; where it cannot say, the column's word // is looked for in its whole text. function test(node, item){ if (!node) return true; if (node.t === 'and') return node.a.every(function(x){ return test(x, item); }); if (node.t === 'or') return node.a.some(function(x){ return test(x, item); }); if (node.t === 'not') return !test(node.a, item); if (node.t === 'level') return item.level ? !!item.level(node.l) : false; if (node.t === 'field' && item.field) { var r = item.field(node.f, node.w); if (r !== null && r !== undefined) return !!r; } return item.text.indexOf(node.w) >= 0; } // The words to mark: those that are wanted, and not those that are not. function words(node, out, neg){ out = out || []; if (!node) return out; if (node.t === 'not') return words(node.a, out, !neg); if (node.t === 'and' || node.t === 'or') { node.a.forEach(function(x){ words(x, out, neg); }); return out; } if (!neg && node.w && out.indexOf(node.w) < 0) out.push(node.w); return out; } // Whether a query asks for any word at all: one for a state alone is about the Findings. function hasWords(node){ if (!node) return false; if (node.t === 'not') return hasWords(node.a); if (node.t === 'and' || node.t === 'or') return node.a.some(hasWords); return node.t !== 'level'; } // A column of a table by the start of its heading or any part of it. function column(heads, f){ for (var i = 0; i < heads.length; i++) { if (heads[i].indexOf(f) === 0) return i; } for (var j = 0; j < heads.length; j++) { if (heads[j].indexOf(f) >= 0) return j; } return -1; } return { fold: fold, parse: parse, test: test, words: words, hasWords: hasWords, column: column }; })(); // A table in the order of any column, and narrowed to the rows that hold a word. // What a cell is sorted as: a number or a date where every cell of the column is // one (as German writes them, and as the Run's own stamps are), else its text. function sortKey(s){ s = (s || '').replace(/\u00a0/g, ' ').trim(); if (!s) return null; var m = s.match(/^(\d{2})\.(\d{2})\.(\d{4})(?:,?\s+(\d{2}):(\d{2})(?::(\d{2}))?)?$/); if (m) return new Date(+m[3], m[2] - 1, +m[1], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0)).getTime(); m = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?)?$/); if (m) return new Date(+m[1], m[2] - 1, +m[3], +(m[4] || 0), +(m[5] || 0), +(m[6] || 0)).getTime(); var t = s.replace(/\s?(%|ms|s|KB|MB|GB|TB|Bit)$/, ''); if (/^-?\d{1,3}(\.\d{3})+(,\d+)?$/.test(t)) return parseFloat(t.replace(/\./g, '').replace(',', '.')); if (/^-?\d+([.,]\d+)?$/.test(t)) return parseFloat(t.replace(',', '.')); return NaN; } function rowItem(table, r){ if (r._ft === undefined) r._ft = Q.fold(r.textContent); return { text: r._ft, field: function(f, w){ var c = Q.column(table._heads || [], f); if (c < 0) return null; var cell = r.cells[c]; return Q.fold(cell ? cell.textContent : '').indexOf(w) >= 0; } }; } each('table.data', function(table){ var body = table.tBodies[0], head = table.tHead; if (!body || !head) return; var rows = Array.prototype.slice.call(body.rows); rows.forEach(function(r, i){ r._i = i; }); var heads = Array.prototype.slice.call(head.rows[0].cells); heads.forEach(function(th, col){ th.title = table.getAttribute('data-sort') || ''; th.addEventListener('click', function(){ var dir = th.getAttribute('data-dir') === '1' ? -1 : (th.getAttribute('data-dir') === '-1' ? 0 : 1); heads.forEach(function(h){ h.removeAttribute('data-dir'); }); var order = rows.slice(); if (dir) { th.setAttribute('data-dir', dir); var keys = rows.map(function(r){ return sortKey(r.cells[col] ? r.cells[col].textContent : ''); }); var numeric = keys.some(function(k){ return k !== null; }) && keys.every(function(k){ return k === null || !isNaN(k); }); order.sort(function(a, b){ var x = a.cells[col] ? a.cells[col].textContent.trim() : '', y = b.cells[col] ? b.cells[col].textContent.trim() : ''; // An empty cell stands last, whichever way the column is sorted. if (!x || !y) return (x ? -1 : 0) + (y ? 1 : 0) || a._i - b._i; var c = numeric ? keys[a._i] - keys[b._i] : x.localeCompare(y, undefined, { numeric: true, sensitivity: 'base' }); return c * dir || a._i - b._i; }); } order.forEach(function(r){ body.appendChild(r); }); regroup(); }); }); // The headings, folded, for Spalte:Wert. var foldedHeads = heads.map(function(h){ return Q.fold(h.textContent.trim()); }); table._heads = foldedHeads; // Rows by a column: its values, or by day or hour of a time, the network of an // address, the server of a path. Each group a line of its own that folds what is // under it, with how many rows are in it; sorting sorts inside the groups, a filter // and a search count what is left of each. var groupBy = null, shut = {}; function cellText(r, c){ var x = r.cells[c]; return x ? x.textContent.replace(/\u00a0/g, ' ').trim() : ''; } function two(n){ return (n < 10 ? '0' : '') + n; } // A date as German writes it or as the Run stamps it, by how it is written: a number is no date. function isTime(s){ return /^(\d{2}\.\d{2}\.\d{4}|\d{4}-\d{2}-\d{2})/.test(s); } function keyOf(r){ var s = cellText(r, groupBy.col); if (!s) return ''; if (groupBy.kind === 'day' || groupBy.kind === 'hour' || groupBy.kind === 'month') { var k = sortKey(s); if (isTime(s) && typeof k === 'number' && !isNaN(k)) { var d = new Date(k), day = two(d.getDate()) + '.' + two(d.getMonth() + 1) + '.' + d.getFullYear(); if (groupBy.kind === 'month') return two(d.getMonth() + 1) + '.' + d.getFullYear(); return groupBy.kind === 'day' ? day : day + ' ' + two(d.getHours()) + ':00'; } } if (groupBy.kind === 'net') { var m = s.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3})\.\d{1,3}/); if (m) return m[1] + '.0/24'; } if (groupBy.kind === 'server') { var u = s.match(/^\\\\([^\\]+)/); if (u) return '\\\\' + u[1]; } return s; } function regroup(){ Array.prototype.forEach.call(body.querySelectorAll('tr.grp'), function(g){ body.removeChild(g); }); rows.forEach(function(r){ r.classList.remove('gx', 'gin', 'galt'); }); if (!groupBy) return; var groups = [], byKey = {}; Array.prototype.forEach.call(body.rows, function(r){ // "INTEL" and "Intel" are one group, under the first way it is written. var label = keyOf(r), k = Q.fold(label).trim(); if (!Object.prototype.hasOwnProperty.call(byKey, k)) { byKey[k] = { k: k, label: label, rows: [], at: null }; groups.push(byKey[k]); } byKey[k].rows.push(r); var t = sortKey(cellText(r, groupBy.col)); if (typeof t === 'number' && !isNaN(t) && (byKey[k].at === null || t > byKey[k].at)) byKey[k].at = t; }); // A time newest first; anything else the largest group first. if (groupBy.kind === 'day' || groupBy.kind === 'hour' || groupBy.kind === 'month') groups.sort(function(a, b){ return (b.at || 0) - (a.at || 0); }); else groups.sort(function(a, b){ return b.rows.length - a.rows.length || a.label.localeCompare(b.label, undefined, { numeric: true, sensitivity: 'base' }); }); // Every row of a group carries its mark, and every second group a tint: which rows // belong together is seen in the table, and not only in the line over them. groups.forEach(function(g, gi){ var alt = gi % 2 === 1; var tr = document.createElement('tr'); tr.className = alt ? 'grp galt' : 'grp'; var td = document.createElement('td'); td.colSpan = heads.length; var name = document.createElement('span'); name.className = 'gk'; name.textContent = g.label || table.getAttribute('data-gempty') || ''; var left = g.rows.filter(function(r){ return r.style.display !== 'none' && !r.classList.contains('sx'); }).length; var n = document.createElement('span'); n.className = 'gn'; n.textContent = (table.getAttribute('data-grows') || '{0}').replace('{0}', left); td.appendChild(name); td.appendChild(n); tr.appendChild(td); tr.hidden = !left; tr._key = g.k; if (shut[g.k]) tr.classList.add('shut'); body.appendChild(tr); g.rows.forEach(function(r){ r.classList.add('gin'); if (alt) r.classList.add('galt'); if (shut[g.k]) r.classList.add('gx'); body.appendChild(r); }); }); } table._regroup = regroup; body.addEventListener('click', function(e){ var g = e.target.closest ? e.target.closest('tr.grp') : null; if (!g) return; shut[g._key] = !shut[g._key]; regroup(); }); var box = table.parentNode && table.parentNode.classList && table.parentNode.classList.contains('evi') ? table.parentNode : table; var bar = document.createElement('div'); bar.className = 'tbar'; box.parentNode.insertBefore(bar, box); // A filter and the groups for a table long enough to want them. if (rows.length >= 8) { var input = document.createElement('input'); input.type = 'search'; input.className = 'search tfilter'; input.placeholder = table.getAttribute('data-filter') || ''; input.title = table.getAttribute('data-filterhelp') || ''; var count = document.createElement('span'); count.className = 'tcount'; bar.appendChild(input); bar.appendChild(count); bar.classList.add('hasfilter'); input.addEventListener('input', function(){ var q = Q.parse(input.value), shown = 0; rows.forEach(function(r){ var keep = Q.test(q, rowItem(table, r)); r.style.display = keep ? '' : 'none'; if (keep) shown++; }); count.textContent = q ? (table.getAttribute('data-shown') || '').replace('{0}', shown).replace('{1}', rows.length) : ''; regroup(); }); // What a column can be grouped by: its values where they repeat, and a time by day // and by hour, an address by its network, a path by its server. var choose = document.createElement('select'); choose.className = 'tgroup'; choose.title = table.getAttribute('data-group') || ''; choose.setAttribute('aria-label', choose.title); var none = document.createElement('option'); none.value = ''; none.textContent = table.getAttribute('data-gnone') || ''; choose.appendChild(none); var add = function(value, label){ var o = document.createElement('option'); o.value = value; o.textContent = label; choose.appendChild(o); }; heads.forEach(function(th, c){ var name = th.textContent.trim(), values = rows.map(function(r){ return cellText(r, c); }).filter(Boolean); if (!values.length) return; var distinct = {}; values.forEach(function(x){ distinct[x] = 1; }); var every = function(re){ return values.every(function(x){ return re.test(x); }); }; var times = values.every(function(x){ var k = sortKey(x); return isTime(x) && typeof k === 'number' && !isNaN(k); }); var hours = times && values.some(function(x){ return /\d{1,2}:\d{2}/.test(x); }); if (Object.keys(distinct).length < values.length) add('value:' + c, name); if (times) add('month:' + c, (table.getAttribute('data-gmonth') || '{0}').replace('{0}', name)); if (times) add('day:' + c, (table.getAttribute('data-gday') || '{0}').replace('{0}', name)); if (hours) add('hour:' + c, (table.getAttribute('data-ghour') || '{0}').replace('{0}', name)); if (every(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/)) add('net:' + c, (table.getAttribute('data-gnet') || '{0}').replace('{0}', name)); if (every(/^\\\\[^\\]+/)) add('server:' + c, (table.getAttribute('data-gserver') || '{0}').replace('{0}', name)); }); if (choose.options.length > 1) { choose.addEventListener('change', function(){ var v = choose.value; shut = {}; groupBy = v ? { kind: v.split(':')[0], col: +v.split(':')[1] } : null; regroup(); }); bar.appendChild(choose); } } // Every table of the evidence can be shown over the whole window, from a sign in its // head; the Report beside it keeps its width. On every table, and not only where one // was measured too wide: a table is measured only once it is open and shown, and the // ones that needed it most were the ones without the sign. var sec = box !== table ? table.closest('details:not(.help)') : null, head = sec ? sec.querySelector('summary') : null; if (head && head.parentNode === sec) { var wide = document.createElement('button'); wide.type = 'button'; wide.className = 'wide'; wide.innerHTML = fullIcon.open; wide.title = table.getAttribute('data-wide') || ''; wide.setAttribute('aria-label', wide.title); // Behind the chip of the rights, and so before the room kept for the help's "?", // which stands over the end of the head. var chip = head.querySelector('.priv, .chip'); head.insertBefore(wide, chip ? chip.nextSibling : null); var copy = document.createElement('button'); copy.type = 'button'; copy.className = 'copy tcopy'; copy.innerHTML = copySign; copy.title = table.getAttribute('data-tcopy') || ''; copy.setAttribute('aria-label', copy.title); copy.setAttribute('data-done', table.getAttribute('data-tcopied') || ''); head.insertBefore(copy, wide); } }); // The help of a Section opened from a "?" in its head, the first of its buttons and in // their line. Without the script the help keeps its own "?", which stands over the end // of the head where no button of the script is. each('#evidence details:not(.help)', function(d){ var help = null, head = null; Array.prototype.forEach.call(d.children, function(c){ if (c.tagName === 'SUMMARY') head = c; else if (c.tagName === 'DETAILS' && c.classList.contains('help')) help = c; }); if (!help || !head) return; var own = help.querySelector('summary'); var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'hbtn'; btn.textContent = '?'; btn.title = own ? own.getAttribute('title') || '' : ''; btn.setAttribute('aria-label', btn.title); btn.setAttribute('aria-expanded', String(help.open)); var first = head.querySelector('.priv, .chip, .tcopy, .wide'); if (first) head.insertBefore(btn, first); else head.appendChild(btn); btn._help = help; }); document.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('summary .hbtn') : null; if (!b) return; // In the head of the fold, and no click on the fold. e.preventDefault(); e.stopPropagation(); var d = b.closest('details:not(.help)'); if (d && !d.open) d.open = true; b._help.open = !b._help.open; b.setAttribute('aria-expanded', String(b._help.open)); }, true); var full = null; function closeFull(){ if (!full) return; full.classList.remove('fullview'); document.body.classList.remove('has-full'); var b = full.querySelector('summary .wide'), t = full.querySelector('table.data'); if (b && t) { b.innerHTML = fullIcon.open; b.title = t.getAttribute('data-wide') || ''; b.setAttribute('aria-label', b.title); } var was = full; full = null; was.scrollIntoView({ block: 'nearest' }); } document.addEventListener('click', function(e){ var b = e.target.closest ? e.target.closest('summary .wide') : null; if (b) { // In the head of the fold, and no click on the fold: the table stays open. e.preventDefault(); e.stopPropagation(); var d = b.closest('details:not(.help)'), t = d ? d.querySelector('table.data') : null; if (!d) return; if (full === d) { closeFull(); return; } closeFull(); full = d; d.open = true; d.classList.add('fullview'); document.body.classList.add('has-full'); if (t) { b.innerHTML = fullIcon.shut; b.title = t.getAttribute('data-narrow') || ''; b.setAttribute('aria-label', b.title); } return; } // A click beside the table, on the dimmed Report, closes it. if (full && e.target.closest && !e.target.closest('details.fullview')) closeFull(); }, true); document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && full) { e.stopImmediatePropagation(); closeFull(); } }, true); // A search in the part of the Report that is shown - an area, an application, the // hints, every Check, what stands out, the evidence or the terms - and through all of // it where asked, or from the overview. Words are all wanted, "in quotes" is one, // -word is not wanted, FAIL WARN INFO OK in capitals narrow the Findings to that // state. Case and umlauts do not count: "domane" finds "Domaene" as written with one. var finder = document.getElementById('finder'); if (finder && nav) { var fcount = document.getElementById('fcount'), fscope = document.getElementById('fscope'), fwide = document.getElementById('fwide'); var hits = [], at = -1, opened = [], shut = [], from = null, everywhere = false, timer = null; var fold = Q.fold; function textOf(el, part){ if (el._fs === undefined) el._fs = fold(part ? part() : el.textContent); return el._fs; } // A Finding's line by its columns: what it is called, what it found, its application, // its area. function rowField(r){ return function(f, w){ var b = r.querySelector('.what b'), v = r.querySelector('.val'), g = r.closest('details.group'), h = g ? g.querySelector('summary b') : null; var by = { prufung: b, befund: b, check: b, wert: v, value: v, bereich: h, area: h }; if (f === 'anwendung' || f === 'app') return fold(r.getAttribute('data-app') || '').indexOf(w) >= 0; for (var k in by) { if (k.indexOf(f) === 0) return fold(by[k] ? by[k].textContent : '').indexOf(w) >= 0; } return null; }; } function rowText(r){ var g = r.closest('details.group'), b = r.querySelector('.what b'), v = r.querySelector('.val'), h = g ? g.querySelector('summary b') : null; return [b ? b.textContent : '', v ? v.textContent : '', r.getAttribute('data-app') || '', h ? h.textContent : ''].join(' '); } function nameOf(pick){ var e = nav.querySelector('[data-pick="' + pick.replace(/"/g, '\\"') + '"] .nm'); return e ? e.textContent : pick; } // Where a search looks: the part shown, or everything. The overview is no part to search in. function home(){ var s = from !== null ? from : view; return everywhere || s === 'overview' || s === 'network' || s === 'find' ? null : s; } function say(){ var s = home(); fscope.textContent = s ? fscope.getAttribute('data-in').replace('{0}', nameOf(s)) : fscope.getAttribute('data-all'); var base = from !== null ? from : view, scoped = base !== 'overview' && base !== 'find'; fwide.hidden = !finder.value.trim() || !scoped; fwide.textContent = everywhere ? fwide.getAttribute('data-back').replace('{0}', nameOf(base)) : fwide.getAttribute('data-all'); } viewShown = function(next){ if (next !== 'find' && from === null) say(); }; function reset(){ hits.forEach(function(m){ var p = m.parentNode; if (p) { p.replaceChild(document.createTextNode(m.textContent), m); p.normalize(); } }); hits = []; at = -1; opened.forEach(function(r){ r.classList.remove('open'); }); opened = []; shut.forEach(function(d){ d.open = false; }); shut = []; each('.sx', function(el){ el.classList.remove('sx'); }); each('table.data', function(tb){ if (tb._regroup) tb._regroup(); }); root.classList.remove('finding'); } stopFind = function(){ if (!finder.value && from === null) return; finder.value = ''; reset(); from = null; everywhere = false; fcount.textContent = ''; say(); }; findIn = function(pick){ if (!finder.value.trim()) return false; reset(); from = pick; everywhere = false; run(); return true; }; function mark(node, words){ if (!node || hits.length >= 2000) return; var walk = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false), list = [], n; while ((n = walk.nextNode())) { if (!(n.parentNode.closest && n.parentNode.closest('button,mark'))) list.push(n); } list.forEach(function(t){ var s = fold(t.nodeValue), cuts = []; words.forEach(function(w){ var i = 0; while ((i = s.indexOf(w, i)) >= 0) { cuts.push([i, i + w.length]); i += w.length; } }); if (!cuts.length || hits.length >= 2000) return; cuts.sort(function(a, b){ return a[0] - b[0]; }); var joined = []; cuts.forEach(function(c){ var l = joined[joined.length - 1]; if (l && c[0] <= l[1]) l[1] = Math.max(l[1], c[1]); else joined.push(c.slice()); }); var text = t.nodeValue, frag = document.createDocumentFragment(), pos = 0; joined.forEach(function(c){ frag.appendChild(document.createTextNode(text.slice(pos, c[0]))); var m = document.createElement('mark'); m.className = 'hit'; m.textContent = text.slice(c[0], c[1]); frag.appendChild(m); hits.push(m); pos = c[1]; }); frag.appendChild(document.createTextNode(text.slice(pos))); t.parentNode.replaceChild(frag, t); }); } // Whether a Finding's line is in the part searched. function inPart(r, s){ if (s === null || s === 'all') return true; var app = r.getAttribute('data-app') || '', g = r.closest('details.group'); if (s === 'info') return r.classList.contains('INFO'); if (s.indexOf('cat:') === 0) return !app && !!g && g.getAttribute('data-cat') === s.slice(4); if (s.indexOf('app:') === 0) return app === s.slice(4); return false; } function run(){ var v = finder.value.trim(); reset(); if (!v) { fcount.textContent = ''; if (from !== null) { var back = from; from = null; everywhere = false; show(back); } say(); return; } if (from === null) from = view === 'find' ? 'all' : view; var q = Q.parse(v), s = home(), parts = [], n = 0, wanted = Q.words(q), anyWord = Q.hasWords(q); var state = function(el){ return function(l){ return el.classList.contains(l); }; }; // A hit only in what a closed line or card hides opens it. var reveal = function(el, seenText){ if (wanted.length && !wanted.some(function(w){ return seenText.indexOf(w) >= 0; }) && !el.classList.contains('open')) { el.classList.add('open'); opened.push(el); } }; root.classList.add('finding'); var words = wanted.slice().sort(function(a, b){ return b.length - a.length; }); if (s === 'action') { // What stands out is searched as it stands there: card by card. each('#ch-action .card', function(c){ if (!Q.test(q, { text: textOf(c), level: state(c) })) { c.classList.add('sx'); return; } n++; var h = c.querySelector('h3'), first = c.querySelector('.parts>li'); reveal(c, fold((h ? h.textContent : '') + ' ' + (first ? first.textContent : ''))); }); show('action'); var lc = chapter('leads'); if (lc) lc.hidden = true; if (words.length) each('#ch-action .card:not(.sx)', function(c){ mark(c, words); }); parts.push(fcount.getAttribute('data-rows').replace('{0}', n)); } else { var rowsN = 0, secN = 0, termN = 0, rowsToo = s === null || s === 'all' || s === 'info' || s.indexOf('cat:') === 0 || s.indexOf('app:') === 0; each('#checks .row', function(r){ if (!(rowsToo && inPart(r, s) && Q.test(q, { text: textOf(r, function(){ return rowText(r); }), level: state(r), field: rowField(r) }))) { r.classList.add('sx'); return; } rowsN++; var b = r.querySelector('.what b'), first = r.querySelector('.val>div'); reveal(r, fold((b ? b.textContent : '') + ' ' + (first ? first.textContent : ''))); }); show('find'); // The evidence: a table whose title is wanted stands whole, any other with the rows that are. // A search for a state alone is a search among the Findings. var evidenceToo = (s === null || s === 'evidence') && anyWord, termsToo = (s === null || s === 'glossary') && anyWord; each('#evidence details:not(.help)', function(d){ var any = false; if (evidenceToo) { if (Q.test(q, { text: fold(d.getAttribute('data-title') || '') })) any = true; else { var tb = d.querySelector('table.data'), trs = d.querySelectorAll('tbody tr:not(.grp)'); Array.prototype.forEach.call(trs, function(tr){ if (Q.test(q, tb ? rowItem(tb, tr) : { text: textOf(tr) })) any = true; else tr.classList.add('sx'); }); if (!trs.length && Q.test(q, { text: textOf(d) })) any = true; } } if (!any) { d.classList.add('sx'); return; } secN++; if (!d.open) { d.open = true; shut.push(d); } }); each('#evidence .kind', function(k){ if (!k.querySelector('details:not(.help):not(.sx)')) k.classList.add('sx'); }); each('#evidence .egroup', function(g){ if (!g.querySelector('.kind:not(.sx)')) g.classList.add('sx'); }); each('#ch-glossary .glossary>div', function(e){ if (termsToo && Q.test(q, { text: textOf(e) })) termN++; else e.classList.add('sx'); }); each('table.data', function(tb){ if (tb._regroup) tb._regroup(); }); var c; if ((c = chapter('checks'))) c.hidden = !rowsN; if ((c = chapter('evidence'))) c.hidden = !secN; if ((c = chapter('glossary'))) c.hidden = !termN; if (words.length) { each('#checks .row:not(.sx)', function(r){ mark(r.querySelector('.what b'), words); mark(r.querySelector('.val'), words); }); each('#evidence details:not(.help):not(.sx)', function(d){ mark(d.querySelector('summary'), words); Array.prototype.forEach.call(d.querySelectorAll('tbody tr:not(.sx):not(.grp)'), function(tr){ mark(tr, words); }); }); each('#ch-glossary .glossary>div:not(.sx)', function(e){ mark(e, words); }); } if (rowsToo) parts.push(fcount.getAttribute('data-rows').replace('{0}', rowsN)); if (s === null || s === 'evidence') parts.push(fcount.getAttribute('data-secs').replace('{0}', secN)); if (s === null || s === 'glossary') parts.push(fcount.getAttribute('data-terms').replace('{0}', termN)); n = rowsN + secN + termN; } // The part searched is the part picked in the tree. each('#nav [data-pick]', function(b){ b.setAttribute('aria-current', String(!everywhere && b.getAttribute('data-pick') === from)); }); fcount.textContent = n ? parts.join(' \u00b7 ') + (hits.length ? ' \u00b7 ' + fcount.getAttribute('data-hits').replace('{0}', hits.length) : '') : fcount.getAttribute('data-none'); say(); window.scrollTo(0, 0); } // From one place found to the next, opening what hides it. function go(step){ if (!hits.length) return; if (at >= 0 && hits[at]) hits[at].classList.remove('now'); at = (at + step + hits.length) % hits.length; var m = hits[at], r = m.closest('.row') || m.closest('.card'); if (r && !r.classList.contains('open')) { r.classList.add('open'); opened.push(r); } var long = m.closest('.long'); if (long) long.classList.add('open'); m.classList.add('now'); m.scrollIntoView({ block: 'center' }); fcount.textContent = fcount.getAttribute('data-at').replace('{0}', at + 1).replace('{1}', hits.length); } fwide.addEventListener('click', function(){ everywhere = !everywhere; run(); finder.focus(); }); // The help of the search, opened by its "?" and closed by it, by Esc or by a click beside it. var fhelp = document.getElementById('fhelp'), fhelptext = document.getElementById('fhelptext'); function helpOpen(on){ if (!fhelp || !fhelptext) return; fhelptext.hidden = !on; fhelp.setAttribute('aria-expanded', String(on)); } if (fhelp) fhelp.addEventListener('click', function(e){ e.stopPropagation(); helpOpen(fhelptext.hidden); }); document.addEventListener('click', function(e){ if (fhelptext && !fhelptext.hidden && !(e.target.closest && e.target.closest('#fhelptext'))) helpOpen(false); }); document.addEventListener('keydown', function(e){ if (e.key === 'Escape' && fhelptext && !fhelptext.hidden) helpOpen(false); }); finder.addEventListener('input', function(){ clearTimeout(timer); timer = setTimeout(run, 180); }); finder.addEventListener('keydown', function(e){ if (e.key === 'Enter') { e.preventDefault(); clearTimeout(timer); if (at < 0 && !hits.length) run(); go(e.shiftKey ? -1 : 1); } else if (e.key === 'Escape') { e.preventDefault(); finder.value = ''; run(); } }); document.addEventListener('keydown', function(e){ var t = e.target.tagName; if (e.key === '/' && t !== 'INPUT' && t !== 'TEXTAREA' && !e.ctrlKey && !e.metaKey && !e.altKey) { e.preventDefault(); finder.focus(); finder.select(); } }); say(); } // A term of the glossary explained where it stands, on the pointer: the first time in // each Finding, card, suspicion and table, and not every time - a page of dotted // words is a page nobody reads. A table is done when it is opened: the evidence of a // large Report is megabytes, and most of it is never opened. var termData = document.getElementById('terms'), terms = [], termRe = null; try { terms = termData ? JSON.parse(termData.textContent) : []; } catch (x) { terms = []; } if (terms.length) { try { termRe = new RegExp('(?<![\\p{L}\\p{N}_])(?:' + terms.map(function(e){ return '(' + e.p + ')'; }).join('|') + ')(?![\\p{L}\\p{N}_])', 'gu'); } catch (x) { termRe = null; } } function explain(box){ if (!termRe || !box || box._terms) return; box._terms = true; var seen = {}, walk = document.createTreeWalker(box, NodeFilter.SHOW_TEXT, null, false), nodes = [], n; while ((n = walk.nextNode())) { var up = n.parentNode; if (up.closest && up.closest('button,abbr,mark,script,style,input,textarea,.helptext,details.help')) continue; nodes.push(n); } nodes.forEach(function(t){ var s = t.nodeValue, m, cuts = []; termRe.lastIndex = 0; while ((m = termRe.exec(s))) { var k = -1; for (var i = 1; i < m.length; i++) { if (m[i] !== undefined) { k = i - 1; break; } } if (k < 0 || seen[k]) continue; seen[k] = true; cuts.push([m.index, m.index + m[0].length, k]); } if (!cuts.length) return; var frag = document.createDocumentFragment(), pos = 0; cuts.forEach(function(c){ frag.appendChild(document.createTextNode(s.slice(pos, c[0]))); var a = document.createElement('abbr'); a.className = 'term'; a.title = terms[c[2]].t + ': ' + terms[c[2]].x; a.textContent = s.slice(c[0], c[1]); frag.appendChild(a); pos = c[1]; }); frag.appendChild(document.createTextNode(s.slice(pos))); t.parentNode.replaceChild(frag, t); }); } if (termRe) { each('#checks .row,#ch-action .card,.leadbox,.leadgap,.runbox,.alert', explain); each('#evidence details[open]:not(.help)', explain); document.addEventListener('toggle', function(e){ var d = e.target; if (d && d.open && d.matches && d.matches('#evidence details:not(.help)')) explain(d); }, true); } // Paper has nothing to click: everything folded is printed open, and folded back after. // Not the help of a table: that is printed where the reader has opened it, and only there. // The whole Report, too: every chapter, and every Finding in full. var before = []; window.addEventListener('beforeprint', function(){ stopFind(); before = []; each('details:not(.help)', function(d){ before.push([d, d.open]); d.open = true; }); if (nav) { root.classList.remove('js'); each('[data-chapter],#checks details.group,#checks .row,#checks .subject', function(el){ el.hidden = false; }); var vn = document.getElementById('viewnote'); if (vn) vn.hidden = true; } }); window.addEventListener('afterprint', function(){ before.forEach(function(p){ p[0].open = p[1]; }); if (nav) { root.classList.add('js'); show(view); } }); })(); </script> '@ |