fix(aio): remove all links from toc titles (#22533)

The previous approach just removed the first `a` tag that
was found, but now that the header-link anchor is not at
the start of the heading, it could fail.

Closes #22493

PR Close #22533
This commit is contained in:
Pete Bacon Darwin 2018-03-01 10:56:32 +00:00 committed by Alex Eagle
parent 51ca643c27
commit 12be311618
2 changed files with 33 additions and 19 deletions

View File

@ -291,23 +291,20 @@ describe('TocService', () => {
}); });
}); });
describe('TocItem for an h2 with anchor link and extra whitespace', () => { describe('TocItem for an h2 with links and extra whitespace', () => {
let docId: string; let docId: string;
let docEl: HTMLDivElement;
let tocItem: TocItem; let tocItem: TocItem;
let expectedTocContent: string;
beforeEach(() => { beforeEach(() => {
docId = 'fizz/buzz/'; docId = 'fizz/buzz/';
expectedTocContent = 'Setup to develop <i>locally</i>.';
// An almost-actual <h2> ... with extra whitespace // An almost-actual <h2> ... with extra whitespace
docEl = callGenToc(` callGenToc(`
<h2 id="setup-to-develop-locally"> <h2 id="setup-to-develop-locally">
<a href="tutorial/toh-pt1#setup-to-develop-locally" aria-hidden="true"> Setup to <a href="moo">develop</a> <i>locally</i>.
<a class="header-link" href="tutorial/toh-pt1#setup-to-develop-locally" aria-hidden="true">
<span class="icon icon-link"></span> <span class="icon icon-link"></span>
</a> </a>
${expectedTocContent}
</h2> </h2>
`, docId); `, docId);
@ -331,7 +328,7 @@ describe('TocService', () => {
it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => { it('should have bypassed HTML sanitizing of heading\'s innerHTML ', () => {
const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer); const domSanitizer: TestDomSanitizer = injector.get(DomSanitizer);
expect(domSanitizer.bypassSecurityTrustHtml) expect(domSanitizer.bypassSecurityTrustHtml)
.toHaveBeenCalledWith(expectedTocContent); .toHaveBeenCalledWith('Setup to develop <i>locally</i>.');
}); });
}); });
}); });
@ -352,13 +349,20 @@ class TestDomSanitizer {
} }
class MockScrollSpyService { class MockScrollSpyService {
$lastInfo: { private $$lastInfo: {
active: Subject<ScrollItem | null>, active: Subject<ScrollItem | null>,
unspy: jasmine.Spy unspy: jasmine.Spy,
}; } | undefined;
get $lastInfo() {
if (!this.$$lastInfo) {
throw new Error('$lastInfo is not yet defined. You must call `spyOn` first.');
}
return this.$$lastInfo;
}
spyOn(headings: HTMLHeadingElement[]): ScrollSpyInfo { spyOn(headings: HTMLHeadingElement[]): ScrollSpyInfo {
return this.$lastInfo = { return this.$$lastInfo = {
active: new Subject<ScrollItem | null>(), active: new Subject<ScrollItem | null>(),
unspy: jasmine.createSpy('unspy'), unspy: jasmine.createSpy('unspy'),
}; };

View File

@ -16,7 +16,7 @@ export interface TocItem {
export class TocService { export class TocService {
tocList = new ReplaySubject<TocItem[]>(1); tocList = new ReplaySubject<TocItem[]>(1);
activeItemIndex = new ReplaySubject<number | null>(1); activeItemIndex = new ReplaySubject<number | null>(1);
private scrollSpyInfo: ScrollSpyInfo | null; private scrollSpyInfo: ScrollSpyInfo | null = null;
constructor( constructor(
@Inject(DOCUMENT) private document: any, @Inject(DOCUMENT) private document: any,
@ -53,15 +53,25 @@ export class TocService {
// This bad boy exists only to strip off the anchor link attached to a heading // This bad boy exists only to strip off the anchor link attached to a heading
private extractHeadingSafeHtml(heading: HTMLHeadingElement) { private extractHeadingSafeHtml(heading: HTMLHeadingElement) {
const a = this.document.createElement('a') as HTMLAnchorElement; const div: HTMLDivElement = this.document.createElement('div');
a.innerHTML = heading.innerHTML; div.innerHTML = heading.innerHTML;
const anchorLink = a.querySelector('a'); const anchorLinks: NodeListOf<HTMLAnchorElement> = div.querySelectorAll('a');
if (anchorLink) { for (let i = 0; i < anchorLinks.length; i++) {
a.removeChild(anchorLink); const anchorLink = anchorLinks[i];
if (!anchorLink.classList.contains('header-link')) {
// this is an anchor that contains actual content that we want to keep
// move the contents of the anchor into its parent
const parent = anchorLink.parentNode!;
while (anchorLink.childNodes.length) {
parent.insertBefore(anchorLink.childNodes[0], anchorLink);
}
}
// now remove the anchor
anchorLink.remove();
} }
// security: the document element which provides this heading content // security: the document element which provides this heading content
// is always authored by the documentation team and is considered to be safe // is always authored by the documentation team and is considered to be safe
return this.domSanitizer.bypassSecurityTrustHtml(a.innerHTML.trim()); return this.domSanitizer.bypassSecurityTrustHtml(div.innerHTML.trim());
} }
private findTocHeadings(docElement: Element): HTMLHeadingElement[] { private findTocHeadings(docElement: Element): HTMLHeadingElement[] {