Skip to content

Commit

Permalink
feat(anchors): Revert rel=nofollow and add invisible title for anchor…
Browse files Browse the repository at this point in the history
… links
  • Loading branch information
makishvili authored and 3y3 committed Jul 7, 2023
1 parent c8e8f0a commit 8ea800d
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 21 deletions.
25 changes: 25 additions & 0 deletions src/scss/_common.scss
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,31 @@
line-height: 0;
}

/*
Hides element visually, but leaves it visible for search crawlers and screen readers
https://www.a11yproject.com/posts/2013-01-11-how-to-hide-content/
https://hugogiraudel.com/2016/10/13/css-hide-and-seek/
*/
.visually-hidden {
position: absolute;

overflow: hidden;
clip: rect(0 0 0 0);

width: 1px;
height: 1px;
margin: -1px;

padding: 0;

white-space: nowrap;

border: 0;

clip-path: inset(100%);
}

// highlight.js colors
--yfm-color-hljs-background: #{$codeBackgroundColor};
--yfm-color-hljs-subst: #444;
Expand Down
20 changes: 12 additions & 8 deletions src/transform/plugins/anchors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import {MarkdownItPluginCb} from '../typings';

const slugify: (str: string, opts: {}) => string = require('slugify');

function createLinkTokens(state: StateCore, id: string, setId = false) {
function createLinkTokens(state: StateCore, id: string, title: string, setId = false) {
const open = new state.Token('link_open', 'a', 1);
const text = new state.Token('text', '', 0);
const close = new state.Token('link_close', 'a', -1);

if (setId) {
Expand All @@ -20,10 +19,15 @@ function createLinkTokens(state: StateCore, id: string, setId = false) {
open.attrSet('href', '#' + id);
open.attrSet('class', 'yfm-anchor');
open.attrSet('aria-hidden', 'true');
open.attrSet('rel', 'nofollow');
text.content = '';

return [open, text, close];
// SEO: render invisible heading title because link must have text content.
const spanOpen = new state.Token('span_open', 'span', 1);
const spanText = new state.Token('text', '', 0);
const spanClose = new state.Token('span_close', 'span', -1);
spanOpen.attrSet('class', 'visually-hidden');
spanText.content = title;

return [open, spanOpen, spanText, spanClose, close];
}

const getCustomIds = (content: string) => {
Expand Down Expand Up @@ -123,15 +127,15 @@ const index: MarkdownItPluginCb<Options> = (
}

const allAnchorIds = customIds ? customIds : [id];

const anchorTitle = removeCustomId(title).replace(/`/g, '');
allAnchorIds.forEach((customId) => {
const setId = id !== customId;
const linkTokens = createLinkTokens(state, customId, setId);
const linkTokens = createLinkTokens(state, customId, anchorTitle, setId);

inlineToken.children?.unshift(...linkTokens);

if (supportGithubAnchors) {
const ghLinkTokens = createLinkTokens(state, ghId, true);
const ghLinkTokens = createLinkTokens(state, ghId, anchorTitle, true);
inlineToken.children?.unshift(...ghLinkTokens);
}
});
Expand Down
33 changes: 20 additions & 13 deletions test/anchors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,19 +22,26 @@ describe('Anchors', () => {
log.clear();
});

it('should add single anchor with auto naming', () => {
expect(transformYfm('## Test\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test"><a href="#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content</p>\n',
);
});

it('should add single anchor', () => {
expect(transformYfm('## Test {#test1}\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
'<h2 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content</p>\n',
);
});

it('should add multiple anchors', () => {
expect(transformYfm('## Test {#test1} {#test2} {#test3}\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test1">' +
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
'<a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>' +
'<a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content</p>\n',
);
});
Expand All @@ -49,9 +56,9 @@ describe('Anchors', () => {
'{% include [test](./mocks/include-anchor.md) %}\n',
),
).toBe(
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content before include</p>\n' +
'<h1 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Title</h1>\n' +
'<h1 id="test1"><a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
'<p>Content</p>\n',
);
});
Expand All @@ -66,20 +73,20 @@ describe('Anchors', () => {
'{% include [test](./mocks/include-multiple-anchors.md) %}\n',
),
).toBe(
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Test</h2>\n' +
'<h2 id="test0"><a href="#test0" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a>Test</h2>\n' +
'<p>Content before include</p>\n' +
'<h1 id="test1">' +
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
'<a href="#test1" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Title</h1>\n' +
'<a id="test3" href="#test3" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
'<a id="test2" href="#test2" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>' +
'<a href="#test1" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Title</span></a>Title</h1>\n' +
'<p>Content</p>\n',
);
});

it('should be transliterated correctly', () => {
expect(transformYfm('## Максимальный размер дисков \n' + '\n' + 'Content\n')).toBe(
'<h2 id="maksimalnyj-razmer-diskov">' +
'<a href="#maksimalnyj-razmer-diskov" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>' +
'<a href="#maksimalnyj-razmer-diskov" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Максимальный размер дисков</span></a>' +
'Максимальный размер дисков' +
'</h2>\n' +
'<p>Content</p>\n',
Expand All @@ -89,7 +96,7 @@ describe('Anchors', () => {
it('should be removed fences after transliteration', () => {
expect(transformYfm('## `Test`\n' + '\n' + 'Content\n')).toBe(
'<h2 id="test">' +
'<a href="#test" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a><code>Test</code>' +
'<a href="#test" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Test</span></a><code>Test</code>' +
'</h2>\n' +
'<p>Content</p>\n',
);
Expand All @@ -106,7 +113,7 @@ describe('Anchors', () => {
),
).toBe(
'<p>Content before include</p>\n' +
'<h2 id="anchor"><a href="#anchor" class="yfm-anchor" aria-hidden="true" rel="nofollow"></a>Subtitle</h2>\n' +
'<h2 id="anchor"><a href="#anchor" class="yfm-anchor" aria-hidden="true"><span class="visually-hidden">Subtitle</span></a>Subtitle</h2>\n' +
'<p>Subcontent</p>\n' +
'<p>After include</p>\n',
);
Expand Down

0 comments on commit 8ea800d

Please sign in to comment.