82 lines
2.4 KiB
TypeScript
82 lines
2.4 KiB
TypeScript
|
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<string, number>();
|
||
|
|
||
|
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<string, number>) {
|
||
|
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}`;
|
||
|
}
|
||
|
}
|
||
|
}
|