1// Hooks for paged.js 2{ 3 // Utils 4 let pandocMeta, pandocMetaToString; 5 { 6 let el = document.getElementById('pandoc-meta'); 7 pandocMeta = el ? JSON.parse(el.firstChild.data) : {}; 8 } 9 10 pandocMetaToString = meta => { 11 let el = document.createElement('div'); 12 el.innerHTML = meta; 13 return el.innerText; 14 }; 15 16 let isString = value => { 17 return typeof value === 'string' || value instanceof String; 18 }; 19 20 let isArray = value => { 21 return value && typeof value === 'object' && value.constructor === Array; 22 }; 23 24 // This hook is an attempt to fix https://github.com/rstudio/pagedown/issues/131 25 // Sometimes, the {break-after: avoid;} declaration applied on headers 26 // lead to duplicated headers. I hate this bug. 27 // This is linked to the way the HTML source is written 28 // When we have the \n character like this: <div>\n<h1>...</h1> 29 // the header may be duplicated. 30 // But, if we have <div><h1>...</h1> without any \n, the problem disappear 31 // I think this handler can fix most of cases 32 // Obviously, we cannot suppress all the \n in the HTML document 33 // because carriage returns are important in <pre> elements. 34 // Tested with Chrome 76.0.3809.100/Windows 35 Paged.registerHandlers(class extends Paged.Handler { 36 constructor(chunker, polisher, caller) { 37 super(chunker, polisher, caller); 38 this.carriageReturn = String.fromCharCode(10); 39 } 40 41 checkNode(node) { 42 if (!node) return; 43 if (node.nodeType !== 3) return; 44 if (node.textContent === this.carriageReturn) { 45 node.remove(); 46 } 47 } 48 49 afterParsed(parsed) { 50 let template = document.querySelector('template').content; 51 const breakAfterAvoidElements = template.querySelectorAll('[data-break-after="avoid"], [data-break-before="avoid"]'); 52 for (let el of breakAfterAvoidElements) { 53 this.checkNode(el.previousSibling); 54 this.checkNode(el.nextSibling); 55 } 56 } 57 }); 58 59 // This hook creates a list of abbreviations 60 // Note: we also could implement this feature using a Pandoc filter 61 Paged.registerHandlers(class extends Paged.Handler { 62 constructor(chunker, polisher, caller) { 63 super(chunker, polisher, caller); 64 } 65 beforeParsed(content) { 66 const abbreviations = content.querySelectorAll('abbr'); 67 if(abbreviations.length === 0) return; 68 const loaTitle = pandocMeta['loa-title'] ? pandocMetaToString(pandocMeta['loa-title']) : 'List of Abbreviations'; 69 const loaId = 'LOA'; 70 const tocList = content.querySelector('.toc ul'); 71 let listOfAbbreviations = document.createElement('div'); 72 let descriptionList = document.createElement('dl'); 73 content.appendChild(listOfAbbreviations); 74 listOfAbbreviations.id = loaId; 75 listOfAbbreviations.classList.add('section', 'front-matter', 'level1', 'loa'); 76 listOfAbbreviations.innerHTML = '<h1>' + loaTitle + '</h1>'; 77 listOfAbbreviations.appendChild(descriptionList); 78 for(let abbr of abbreviations) { 79 if(!abbr.title) continue; 80 let term = document.createElement('dt'); 81 let definition = document.createElement('dd'); 82 descriptionList.appendChild(term); 83 descriptionList.appendChild(definition); 84 term.innerHTML = abbr.innerHTML; 85 definition.innerText = abbr.title; 86 } 87 if (tocList) { 88 const loaTOCItem = document.createElement('li'); 89 loaTOCItem.innerHTML = '<a href="#' + loaId + '">' + loaTitle + '</a>'; 90 tocList.appendChild(loaTOCItem); 91 } 92 } 93 }); 94 95 // This hook moves the sections of class front-matter in the div.front-matter-container 96 Paged.registerHandlers(class extends Paged.Handler { 97 constructor(chunker, polisher, caller) { 98 super(chunker, polisher, caller); 99 } 100 101 beforeParsed(content) { 102 const frontMatter = content.querySelector('.front-matter-container'); 103 if (!frontMatter) return; 104 105 // move front matter sections in the front matter container 106 const frontMatterSections = content.querySelectorAll('.level1.front-matter'); 107 for (const section of frontMatterSections) { 108 frontMatter.appendChild(section); 109 } 110 111 // add the class front-matter-ref to any <a></a> element 112 // referring to an entry in the front matter 113 const anchors = content.querySelectorAll('a[href^="#"]:not([href*=":"])'); 114 for (const a of anchors) { 115 const ref = a.getAttribute('href').replace(/^#/, ''); 116 const element = content.getElementById(ref); 117 if (frontMatter.contains(element)) a.classList.add('front-matter-ref'); 118 } 119 120 // update the toc, lof and lot for front matter sections 121 const frontMatterSectionsLinks = content.querySelectorAll('.toc .front-matter-ref, .lof .front-matter-ref, .lot .front-matter-ref'); 122 for (let i = frontMatterSectionsLinks.length - 1; i >= 0; i--) { 123 const listItem = frontMatterSectionsLinks[i].parentNode; 124 const list = listItem.parentNode; 125 list.insertBefore(listItem, list.firstChild); 126 } 127 } 128 }); 129 130 // This hook expands the links in the lists of figures and tables 131 Paged.registerHandlers(class extends Paged.Handler { 132 constructor(chunker, polisher, caller) { 133 super(chunker, polisher, caller); 134 } 135 136 beforeParsed(content) { 137 const items = content.querySelectorAll('.lof li, .lot li'); 138 for (const item of items) { 139 const anchor = item.firstChild; 140 anchor.innerText = item.innerText; 141 item.innerText = ''; 142 item.append(anchor); 143 } 144 } 145 }); 146 147 // This hook adds spans for leading symbols 148 Paged.registerHandlers(class extends Paged.Handler { 149 constructor(chunker, polisher, caller) { 150 super(chunker, polisher, caller); 151 } 152 153 beforeParsed(content) { 154 const anchors = content.querySelectorAll('.toc a, .lof a, .lot a'); 155 for (const a of anchors) { 156 a.innerHTML = a.innerHTML + '<span class="leaders"></span>'; 157 } 158 } 159 }); 160 161 // This hook appends short titles spans 162 Paged.registerHandlers(class extends Paged.Handler { 163 constructor(chunker, polisher, caller) { 164 super(chunker, polisher, caller); 165 } 166 167 beforeParsed(content) { 168 /* A factory returning a function that appends short titles spans. 169 The text content of these spans are reused for running titles (see default.css). 170 Argument: level - An integer between 1 and 6. 171 */ 172 function appendShortTitleSpans(level) { 173 return () => { 174 const divs = Array.from(content.querySelectorAll('.level' + level)); 175 176 function addSpan(div) { 177 const mainHeader = div.getElementsByTagName('h' + level)[0]; 178 if (!mainHeader) return; 179 const mainTitle = mainHeader.textContent; 180 const spanSectionNumber = mainHeader.getElementsByClassName('header-section-number')[0]; 181 const mainNumber = !!spanSectionNumber ? spanSectionNumber.textContent : ''; 182 const runningTitle = 'shortTitle' in div.dataset ? mainNumber + ' ' + div.dataset.shortTitle : mainTitle; 183 const span = document.createElement('span'); 184 span.className = 'shorttitle' + level; 185 span.innerText = runningTitle; 186 span.style.display = "none"; 187 mainHeader.appendChild(span);
188 if (level == 1 && div.querySelector('.level2') === null) { 189 let span2 = document.createElement('span'); 190 span2.className = 'shorttitle2'; 191 span2.innerText = ' '; 192 span2.style.display = "none"; 193 span.insertAdjacentElement('afterend', span2); 194 } 195 } 196 197 for (const div of divs) { 198 addSpan(div); 199 } 200 }; 201 } 202 203 appendShortTitleSpans(1)(); 204 appendShortTitleSpans(2)(); 205 } 206 }); 207 208 // Footnotes support 209 Paged.registerHandlers(class extends Paged.Handler { 210 constructor(chunker, polisher, caller) { 211 super(chunker, polisher, caller); 212 213 this.splittedParagraphRefs = []; 214 } 215 216 beforeParsed(content) { 217 // remove footnotes in toc, lof, lot 218 // see https://github.com/rstudio/pagedown/issues/54 219 let removeThese = content.querySelectorAll('.toc .footnote, .lof .footnote, .lot .footnote'); 220 for (const el of removeThese) { 221 el.remove(); 222 } 223 224 let footnotes = content.querySelectorAll('.footnote'); 225 226 for (let footnote of footnotes) { 227 let parentElement = footnote.parentElement; 228 let footnoteCall = document.createElement('a'); 229 let footnoteNumber = footnote.dataset.pagedownFootnoteNumber; 230 231 footnoteCall.className = 'footnote-ref'; // same class as Pandoc 232 footnoteCall.setAttribute('id', 'fnref' + footnoteNumber); // same notation as Pandoc 233 footnoteCall.setAttribute('href', '#' + footnote.id); 234 footnoteCall.innerHTML = '<sup>' + footnoteNumber +'</sup>'; 235 parentElement.insertBefore(footnoteCall, footnote); 236 237 // Here comes a hack. Fortunately, it works with Chrome and FF. 238 let handler = document.createElement('p'); 239 handler.className = 'footnoteHandler'; 240 parentElement.insertBefore(handler, footnote); 241 handler.appendChild(footnote); 242 handler.style.display = 'inline-block'; 243 handler.style.width = '100%'; 244 handler.style.float = 'right'; 245 handler.style.pageBreakInside = 'avoid'; 246 } 247 } 248 249 afterPageLayout(pageFragment, page, breakToken) { 250 function hasItemParent(node) { 251 if (node.parentElement === null) { 252 return false; 253 } else { 254 if (node.parentElement.tagName === 'LI') { 255 return true; 256 } else { 257 return hasItemParent(node.parentElement); 258 } 259 } 260 } 261 // If a li item is broken, we store the reference of the p child element 262 // see https://github.com/rstudio/pagedown/issues/23#issue-376548000 263 if (breakToken !== undefined) { 264 if (breakToken.node.nodeName === "#text" && hasItemParent(breakToken.node)) { 265 this.splittedParagraphRefs.push(breakToken.node.parentElement.dataset.ref); 266 } 267 } 268 } 269 270 afterRendered(pages) { 271 for (let page of pages) { 272 const footnotes = page.element.querySelectorAll('.footnote'); 273 if (footnotes.length === 0) { 274 continue; 275 } 276 277 const pageContent = page.element.querySelector('.pagedjs_page_content'); 278 let hr = document.createElement('hr'); 279 let footnoteArea = document.createElement('div'); 280 281 pageContent.style.display = 'flex'; 282 pageContent.style.flexDirection = 'column'; 283 284 hr.className = 'footnote-break'; 285 hr.style.marginTop = 'auto'; 286 hr.style.marginBottom = 0; 287 hr.style.marginLeft = 0; 288 hr.style.marginRight = 'auto'; 289 pageContent.appendChild(hr); 290 291 footnoteArea.className = 'footnote-area'; 292 pageContent.appendChild(footnoteArea); 293 294 for (let footnote of footnotes) { 295 let handler = footnote.parentElement; 296 297 footnoteArea.appendChild(footnote); 298 handler.parentNode.removeChild(handler); 299 300 footnote.innerHTML = '<sup>' + footnote.dataset.pagedownFootnoteNumber + '</sup>' + footnote.innerHTML; 301 footnote.style.fontSize = 'x-small'; 302 footnote.style.marginTop = 0; 303 footnote.style.marginBottom = 0; 304 footnote.style.paddingTop = 0; 305 footnote.style.paddingBottom = 0; 306 footnote.style.display = 'block'; 307 } 308 } 309 310 for (let ref of this.splittedParagraphRefs) { 311 let paragraphFirstPage = document.querySelector('[data-split-to="' + ref + '"]'); 312 // We test whether the paragraph is empty 313 // see https://github.com/rstudio/pagedown/issues/23#issue-376548000 314 if (paragraphFirstPage.innerText === "") { 315 paragraphFirstPage.parentElement.style.display = "none"; 316 let paragraphSecondPage = document.querySelector('[data-split-from="' + ref + '"]'); 317 paragraphSecondPage.parentElement.style.setProperty('list-style', 'inherit', 'important'); 318 } 319 } 320 } 321 }); 322
323 // Support for "Chapter " label on section with class `.chapter` 324 Paged.registerHandlers(class extends Paged.Handler { 325 constructor(chunker, polisher, caller) { 326 super(chunker, polisher, caller); 327 328 this.options = pandocMeta['chapter_name']; 329 330 let styles; 331 if (isString(this.options)) { 332 this.options = pandocMetaToString(this.options); 333 styles = ` 334 :root { 335 --chapter-name-before: "${this.options}"; 336 } 337 `; 338 } 339 if (isArray(this.options)) { 340 this.options = this.options.map(pandocMetaToString); 341 styles = ` 342 :root { 343 --chapter-name-before: "${this.options[0]}"; 344 --chapter-name-after: "${this.options[1]}"; 345 } 346 `; 347 } 348 if (styles) polisher.insert(styles); 349 } 350 351 beforeParsed(content) { 352 const tocAnchors = content.querySelectorAll('.toc a[href^="#"]:not([href*=":"]'); 353 for (const anchor of tocAnchors) { 354 const ref = anchor.getAttribute('href').replace(/^#/, ''); 355 const element = content.getElementById(ref); 356 if (element.classList.contains('chapter')) { 357 anchor.classList.add('chapter-ref'); 358 } 359 } 360 } 361 }); 362 363 // Main text line numbering, 364 // see https://github.com/rstudio/pagedown/issues/115 365 // Original idea: Julien Taquet, thanks! 366 Paged.registerHandlers(class extends Paged.Handler { 367 constructor(chunker, polisher, caller) { 368 super(chunker, polisher, caller); 369 370 // get the number-lines option from Pandoc metavariables 371 this.options = pandocMeta['number-lines']; 372 // quit early if the "number-lines" option is false or missing 373 if (!this.options) return; 374 // retrieve the selector if provided, otherwise use the default selector 375 this.selector = this.options.selector ? pandocMetaToString(this.options.selector) : '.level1:not(.front-matter) h1, .level1 h2, .level1 h3, .level1 p'; 376 377 const styles = ` 378 :root { 379 --line-numbers-padding-right: 10px; 380 --line-numbers-font-size: 8pt; 381 } 382 .pagedown-linenumbers-container { 383 position: absolute; 384 margin-top: var(--pagedjs-margin-top); 385 right: calc(var(--pagedjs-width) - var(--pagedjs-margin-left)); 386 } 387 .maintextlinenumbers { 388 position: absolute; 389 right: 0; 390 text-align: right; 391 padding-right: var(--line-numbers-padding-right); 392 font-size: var(--line-numbers-font-size); 393 } 394 `; 395 polisher.insert(styles); 396 397 this.resetLinesCounter(); 398 } 399 400 appendLineNumbersContainer(page) { 401 const pagebox = page.element.querySelector('.pagedjs_pagebox'); 402 const lineNumbersContainer = document.createElement('div'); 403 lineNumbersContainer.classList.add('pagedown-linenumbers-container'); 404 405 return pagebox.appendChild(lineNumbersContainer); 406 } 407 408 lineHeight(element) { 409 // If the document stylesheet does not define a value for line-height, 410 // Blink returns "normal". Therefore, parseInt may return NaN. 411 return parseInt(getComputedStyle(element).lineHeight); 412 } 413 414 innerHeight(element) { 415 let outerHeight = element.getBoundingClientRect().height; 416 let {borderTopWidth, 417 borderBottomWidth, 418 paddingTop, 419 paddingBottom} = getComputedStyle(element); 420 421 borderTopWidth = parseFloat(borderTopWidth); 422 borderBottomWidth = parseFloat(borderBottomWidth); 423 paddingTop = parseFloat(paddingTop); 424 paddingBottom = parseFloat(paddingBottom); 425 426 return Math.round(outerHeight - borderTopWidth - borderBottomWidth - paddingTop - paddingBottom); 427 } 428 429 arrayOfInt(from, length) { 430 // adapted from https://stackoverflow.com/a/50234108/6500804 431 return Array.from(Array(length).keys(), n => n + from); 432 } 433 434 incrementLinesCounter(value) { 435 this.linesCounter = this.linesCounter + value; 436 } 437 438 resetLinesCounter() { 439 this.linesCounter = 0; 440 } 441 442 isDisplayMath(element) { 443 const nodes = element.childNodes; 444 if (nodes.length != 1) return false; 445 return (nodes[0].nodeName === 'SPAN') && (nodes[0].classList.value === 'math display'); 446 } 447 448 afterRendered(pages) { 449 if (!this.options) return; 450 451 for (let page of pages) { 452 const lineNumbersContainer = this.appendLineNumbersContainer(page); 453 const pageAreaY = page.area.getBoundingClientRect().y; 454 let elementsToNumber = page.area.querySelectorAll(this.selector); 455 456 for (let element of elementsToNumber) {
457 // Do not add line numbers for display math environment 458 if (this.isDisplayMath(element)) continue; 459 460 // Try to retrieve line height 461 const lineHeight = this.lineHeight(element); 462 // Test against lineHeight method returns NaN 463 if (!lineHeight) { 464 console.warn('Failed to compute line height value on "' + page.id + '".'); 465 continue; 466 } 467 468 const innerHeight = this.innerHeight(element); 469 470 // Number of lines estimation 471 // There is no built-in method to detect the number of lines in a block. 472 // The main caveat is that an actual line height can differ from 473 // the line-height CSS property. 474 // Mixed fonts, subscripts, superscripts, inline math... can increase 475 // the actual line height. 476 // Here, we divide the inner height of the block by the line-height 477 // computed property and round to the floor to take into account that 478 // sometimes the actual line height is greater than its property value. 479 // This is far from perfect and can be easily broken especially by 480 // inline math. 481 const nLines = Math.floor(innerHeight / lineHeight); 482 483 // do not add line numbers for void paragraphs 484 if (nLines <= 0) continue; 485 486 const linenumbers = document.createElement('div'); 487 lineNumbersContainer.appendChild(linenumbers); 488 linenumbers.classList.add('maintextlinenumbers'); 489 490 const elementY = element.getBoundingClientRect().y; 491 linenumbers.style.top = (elementY - pageAreaY) + 'px'; 492 493 const cs = getComputedStyle(element); 494 const paddingTop = parseFloat(cs.paddingTop) + parseFloat(cs.borderTopWidth); 495 linenumbers.style.paddingTop = paddingTop + 'px'; 496 497 linenumbers.style.lineHeight = cs.lineHeight; 498 499 linenumbers.innerHTML = this.arrayOfInt(this.linesCounter + 1, nLines) 500 .reduce((t, v) => t + '<br />' + v); 501 this.incrementLinesCounter(nLines); 502 } 503 504 if (this.options['reset-page']) { 505 this.resetLinesCounter(); 506 } 507 } 508 } 509 }); 510}
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.