From 8ea800d0a630ecd3a765bed493e13cf766c53912 Mon Sep 17 00:00:00 2001 From: VadimMakishvili Date: Fri, 7 Jul 2023 14:10:18 +0300 Subject: [PATCH] feat(anchors): Revert rel=nofollow and add invisible title for anchor links --- src/scss/_common.scss | 25 +++++++++++++++++++ src/transform/plugins/anchors/index.ts | 20 +++++++++------- test/anchors.test.ts | 33 ++++++++++++++++---------- 3 files changed, 57 insertions(+), 21 deletions(-) diff --git a/src/scss/_common.scss b/src/scss/_common.scss index 88c8d9f2..7b1a3948 100644 --- a/src/scss/_common.scss +++ b/src/scss/_common.scss @@ -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; diff --git a/src/transform/plugins/anchors/index.ts b/src/transform/plugins/anchors/index.ts index 6b80ab33..a405961d 100644 --- a/src/transform/plugins/anchors/index.ts +++ b/src/transform/plugins/anchors/index.ts @@ -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) { @@ -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) => { @@ -123,15 +127,15 @@ const index: MarkdownItPluginCb = ( } 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); } }); diff --git a/test/anchors.test.ts b/test/anchors.test.ts index a4e024ac..7f42eadc 100644 --- a/test/anchors.test.ts +++ b/test/anchors.test.ts @@ -22,9 +22,16 @@ describe('Anchors', () => { log.clear(); }); + it('should add single anchor with auto naming', () => { + expect(transformYfm('## Test\n' + '\n' + 'Content\n')).toBe( + '

Test

\n' + + '

Content

\n', + ); + }); + it('should add single anchor', () => { expect(transformYfm('## Test {#test1}\n' + '\n' + 'Content\n')).toBe( - '

Test

\n' + + '

Test

\n' + '

Content

\n', ); }); @@ -32,9 +39,9 @@ describe('Anchors', () => { it('should add multiple anchors', () => { expect(transformYfm('## Test {#test1} {#test2} {#test3}\n' + '\n' + 'Content\n')).toBe( '

' + - '' + - '' + - 'Test

\n' + + '' + + '' + + 'Test\n' + '

Content

\n', ); }); @@ -49,9 +56,9 @@ describe('Anchors', () => { '{% include [test](./mocks/include-anchor.md) %}\n', ), ).toBe( - '

Test

\n' + + '

Test

\n' + '

Content before include

\n' + - '

Title

\n' + + '

Title

\n' + '

Content

\n', ); }); @@ -66,12 +73,12 @@ describe('Anchors', () => { '{% include [test](./mocks/include-multiple-anchors.md) %}\n', ), ).toBe( - '

Test

\n' + + '

Test

\n' + '

Content before include

\n' + '

' + - '' + - '' + - 'Title

\n' + + '' + + '' + + 'Title\n' + '

Content

\n', ); }); @@ -79,7 +86,7 @@ describe('Anchors', () => { it('should be transliterated correctly', () => { expect(transformYfm('## Максимальный размер дисков \n' + '\n' + 'Content\n')).toBe( '

' + - '' + + '' + 'Максимальный размер дисков' + '

\n' + '

Content

\n', @@ -89,7 +96,7 @@ describe('Anchors', () => { it('should be removed fences after transliteration', () => { expect(transformYfm('## `Test`\n' + '\n' + 'Content\n')).toBe( '

' + - 'Test' + + 'Test' + '

\n' + '

Content

\n', ); @@ -106,7 +113,7 @@ describe('Anchors', () => { ), ).toBe( '

Content before include

\n' + - '

Subtitle

\n' + + '

Subtitle

\n' + '

Subcontent

\n' + '

After include

\n', );