import { Inject, Injectable } from '@angular/core'; import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { ReplaySubject } from 'rxjs/ReplaySubject'; import { DocumentContents } from 'app/documents/document.service'; export interface TocItem { content: SafeHtml; href: string; isSecondary?: boolean; level: string; title: string; } @Injectable() export class TocService { tocList: TocItem[]; constructor(@Inject(DOCUMENT) private document: any, private domSanitizer: DomSanitizer) { } genToc(docElement: Element, docId = '') { const tocList = this.tocList = []; if (!docElement) { return; } const headings = docElement.querySelectorAll('h2,h3'); const idMap = new Map(); for (let i = 0; i < headings.length; i++) { const heading = headings[i] as HTMLHeadingElement; // skip if heading class is 'no-toc' if (/(no-toc|notoc)/i.test(heading.className)) { continue; } const id = this.getId(heading, idMap); const toc: TocItem = { content: this.extractHeadingSafeHtml(heading), href: `${docId}#${id}`, level: heading.tagName.toLowerCase(), title: heading.innerText.trim(), }; tocList.push(toc); } } reset() { this.tocList = []; } // This bad boy exists only to strip off the anchor link attached to a heading private extractHeadingSafeHtml(heading: HTMLHeadingElement) { const a = this.document.createElement('a') as HTMLAnchorElement; a.innerHTML = heading.innerHTML; const anchorLink = a.querySelector('a'); if (anchorLink) { a.removeChild(anchorLink); } // security: the document element which provides this heading content // is always authored by the documentation team and is considered to be safe return this.domSanitizer.bypassSecurityTrustHtml(a.innerHTML.trim()); } // Extract the id from the heading; create one if necessary // Is it possible for a heading to lack an id? private getId(h: HTMLHeadingElement, idMap: Map) { let id = h.id; if (id) { addToMap(id); } else { id = h.innerText.toLowerCase().replace(/\W+/g, '-'); id = addToMap(id); h.id = id; } return id; // Map guards against duplicate id creation. function addToMap(key: string) { const count = idMap[key] = idMap[key] ? idMap[key] + 1 : 1; return count === 1 ? key : `${key}-${count}`; } } }