fix(aio): fix window title on Home page (#20440)

Using `display: none` on the `<h1>` causes `innerText` to not work as expected
and include the icon ligature (`link`) in the title. This caused the window
title on the angular.io Home page to appear as "Angular - link".
This commit fixes it by not generating anchors at all for headings with the
`no-anchor` class.

Fixes #20427

PR Close #20440
This commit is contained in:
George Kalpakas 2017-11-15 03:01:00 +02:00 committed by Miško Hevery
parent c28b52187a
commit 7e38f4fd1f
9 changed files with 98 additions and 35 deletions

View File

@ -25,7 +25,7 @@
<!-- MAIN CONTENT -->
<article>
<h1 class="no-toc" style="display: none"></h1>
<h1 class="no-anchor no-toc" style="display: none"></h1>
<div class="home-rows">

View File

@ -15,6 +15,16 @@ describe('site App', function() {
expect(page.getDocViewerText()).toMatch(/Progressive web apps/i);
});
it('should set appropriate window titles', () => {
expect(browser.getTitle()).toBe('Angular');
page.getTopMenuLink('features').click();
expect(browser.getTitle()).toBe('Angular - FEATURES & BENEFITS');
page.homeLink.click();
expect(browser.getTitle()).toBe('Angular');
});
it('should show the tutorial index page at `/tutorial/` after jitterbugging through features', () => {
// check that we can navigate directly to the tutorial page
page.navigateTo('tutorial/');

View File

@ -5,6 +5,7 @@ const githubRegex = /https:\/\/github.com\/angular\/angular\//;
export class SitePage {
links = element.all(by.css('md-toolbar a'));
homeLink = element(by.css('a.home'));
docsMenuLink = element(by.cssContainingText('aio-top-menu a', 'Docs'));
docViewer = element(by.css('aio-doc-viewer'));
codeExample = element.all(by.css('aio-doc-viewer pre > code'));

View File

@ -126,7 +126,6 @@
"lunr": "^2.1.0",
"protractor": "^5.2.0",
"rehype": "^4.0.0",
"rehype-autolink-headings": "^2.0.0",
"rehype-slug": "^2.0.0",
"remark": "^7.0.0",
"remark-html": "^6.0.0",

View File

@ -361,6 +361,22 @@ describe('DocViewerComponent', () => {
fixture.detectChanges();
expect(titleService.setTitle).toHaveBeenCalledWith('Angular - Text Content');
});
it('should still use `innerText` if available but empty', () => {
const querySelector_ = docViewerEl.querySelector;
spyOn(docViewerEl, 'querySelector').and.callFake((selector: string) => {
const elem = querySelector_.call(docViewerEl, selector);
Object.defineProperties(elem, {
innerText: { value: '' },
textContent: { value: 'Text Content' }
});
return elem;
});
setCurrentDoc('<h1><i style="visibility: hidden">link</i></h1>Some content');
fixture.detectChanges();
expect(titleService.setTitle).toHaveBeenCalledWith('Angular');
});
});
describe('TOC', () => {

View File

@ -92,17 +92,19 @@ export class DocViewerComponent implements DoCheck, OnDestroy {
private addTitleAndToc(docId: string) {
this.tocService.reset();
let title = '';
const titleEl = this.hostElement.querySelector('h1');
let title = '';
// Only create TOC for docs with an <h1> title
// If you don't want a TOC, add "no-toc" class to <h1>
if (titleEl) {
title = (titleEl.innerText || titleEl.textContent).trim();
title = (typeof titleEl.innerText === 'string') ? titleEl.innerText : titleEl.textContent;
if (!/(no-toc|notoc)/i.test(titleEl.className)) {
this.tocService.genToc(this.hostElement, docId);
titleEl.insertAdjacentHTML('afterend', '<aio-toc class="embedded"></aio-toc>');
}
}
this.titleService.setTitle(title ? `Angular - ${title}` : 'Angular');
}

View File

@ -1,9 +1,34 @@
const has = require('hast-util-has-property');
const is = require('hast-util-is-element');
const slug = require('rehype-slug');
const link = require('rehype-autolink-headings');
const visit = require('unist-util-visit');
/**
* Get remark to inject anchors into headings
* Get remark to add IDs to headings and inject anchors into them.
* This is a stripped-down equivalent of [rehype-autolink-headings](https://github.com/wooorm/rehype-autolink-headings)
* that supports ignoring headings with the `no-anchor` class.
*/
const HEADINGS = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
const NO_ANCHOR_CLASS = 'no-anchor';
const clone = obj => JSON.parse(JSON.stringify(obj));
const hasClass = (node, cls) => {
const className = node.properties.className;
return className && className.includes(cls);
};
const link = options =>
tree => visit(tree, node => {
if (is(node, HEADINGS) && has(node, 'id') && !hasClass(node, NO_ANCHOR_CLASS)) {
node.children.unshift({
type: 'element',
tagName: 'a',
properties: Object.assign(clone(options.properties), {href: `#${node.properties.id}`}),
children: clone(options.content)
});
}
});
module.exports = [
slug,
[link, {
@ -12,11 +37,13 @@ module.exports = [
className: ['header-link'],
'aria-hidden': 'true'
},
content: {
type: 'element',
tagName: 'i',
properties: {className: ['material-icons']},
children: [{ type: 'text', value: 'link' }]
}
content: [
{
type: 'element',
tagName: 'i',
properties: {className: ['material-icons']},
children: [{ type: 'text', value: 'link' }]
}
]
}]
];

View File

@ -14,20 +14,37 @@ describe('autolink-headings postprocessor', () => {
});
it('should add anchors to headings', () => {
const docs = [ {
docType: 'a',
renderedContent: `
<h1>Heading 1</h2>
<h2>Heading with <strong>bold</strong></h2>
<h3>Heading with encoded chars &#x26;</h3>
`
}];
const originalContent = `
<h1>Heading 1</h2>
<h2>Heading with <strong>bold</strong></h2>
<h3>Heading with encoded chars &#x26;</h3>
`;
const processedContent = `
<h1 id="heading-1"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-1"><i class="material-icons">link</i></a>Heading 1</h1>
<h2 id="heading-with-bold"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-bold"><i class="material-icons">link</i></a>Heading with <strong>bold</strong></h2>
<h3 id="heading-with-encoded-chars-"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-encoded-chars-"><i class="material-icons">link</i></a>Heading with encoded chars &#x26;</h3>
`;
const docs = [{docType: 'a', renderedContent: originalContent}];
processor.$process(docs);
expect(docs[0].renderedContent).toEqual(`
<h1 id="heading-1"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-1"><i class="material-icons">link</i></a>Heading 1</h1>
<h2 id="heading-with-bold"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-bold"><i class="material-icons">link</i></a>Heading with <strong>bold</strong></h2>
<h3 id="heading-with-encoded-chars-"><a title="Link to this heading" class="header-link" aria-hidden="true" href="#heading-with-encoded-chars-"><i class="material-icons">link</i></a>Heading with encoded chars &#x26;</h3>
`);
expect(docs[0].renderedContent).toBe(processedContent);
});
it('should ignore headings with the `no-anchor` class', () => {
const originalContent = `
<h1 class="no-anchor">Heading 1</h2>
<h2 class="no-anchor">Heading with <strong>bold</strong></h2>
<h3 class="no-anchor">Heading with encoded chars &#x26;</h3>
`;
const processedContent = `
<h1 class="no-anchor" id="heading-1">Heading 1</h1>
<h2 class="no-anchor" id="heading-with-bold">Heading with <strong>bold</strong></h2>
<h3 class="no-anchor" id="heading-with-encoded-chars-">Heading with encoded chars &#x26;</h3>
`;
const docs = [{docType: 'a', renderedContent: originalContent}];
processor.$process(docs);
expect(docs[0].renderedContent).toBe(processedContent);
});
});

View File

@ -2977,7 +2977,7 @@ express@^4.13.3:
utils-merge "1.0.1"
vary "~1.1.2"
extend@3, extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
@ -6999,15 +6999,6 @@ regjsparser@^0.1.4:
dependencies:
jsesc "~0.5.0"
rehype-autolink-headings@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/rehype-autolink-headings/-/rehype-autolink-headings-2.0.2.tgz#48c7161b1a1020e942c758eb6b2c55cb1bc504d0"
dependencies:
extend "^3.0.1"
hast-util-has-property "^1.0.0"
hast-util-is-element "^1.0.0"
unist-util-visit "^1.1.0"
rehype-parse@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/rehype-parse/-/rehype-parse-3.1.0.tgz#7f5227a597a3f39fc4b938646161539c444ee728"