Skip to content

Commit

Permalink
Bring back heading anchor links (withastro#5610)
Browse files Browse the repository at this point in the history
  • Loading branch information
delucis authored Dec 5, 2023
1 parent 70660d1 commit 2429811
Show file tree
Hide file tree
Showing 5 changed files with 177 additions and 0 deletions.
3 changes: 3 additions & 0 deletions astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import rehypeSlug from 'rehype-slug';
import remarkSmartypants from 'remark-smartypants';

import { sitemap } from './integrations/sitemap';
import { rehypeAutolink } from './plugins/rehype-autolink';
import { rehypeOptimizeStatic } from './plugins/rehype-optimize-static';
import { rehypeTasklistEnhancer } from './plugins/rehype-tasklist-enhancer';
import { remarkFallbackLang } from './plugins/remark-fallback-lang';
Expand Down Expand Up @@ -82,6 +83,8 @@ export default defineConfig({
],
rehypePlugins: [
rehypeSlug,
// This adds links to headings
...rehypeAutolink(),
// Tweak GFM task list syntax
rehypeTasklistEnhancer(),
// Collapse static parts of the hast to html
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
"canvas-confetti": "^1.6.0",
"jsdoc-api": "^7.1.1",
"minimatch": "^9.0.3",
"rehype-autolink-headings": "^6.1.1",
"rehype-slug": "^5.0.1",
"remark-gfm": "^3.0.1",
"remark-smartypants": "^2.0.0",
Expand Down
87 changes: 87 additions & 0 deletions plugins/rehype-autolink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { Root } from 'hast';
import { toString } from 'hast-util-to-string';
import { h } from 'hastscript';
import { escape } from 'html-escaper';
import { resolve as nodeResolve } from 'node:path';
import rehypeAutolinkHeadings, { type Options as AutolinkOptions } from 'rehype-autolink-headings';
import type { Transformer } from 'unified';
import { visit } from 'unist-util-visit';
import type { UILanguageKeys } from '../src/i18n/translation-checkers';
import { useTranslationsForLang } from '../src/i18n/util';
import { getLanguageCodeFromPathname, mdFilePathToUrl } from './remark-fallback-lang';
import type { RehypePlugins } from 'astro';

const AnchorLinkIcon = h(
'span',
{ ariaHidden: 'true', class: 'anchor-icon' },
h(
'svg',
{ width: 16, height: 16, viewBox: '0 0 24 24' },
h('path', {
fill: 'currentcolor',
d: 'm12.11 15.39-3.88 3.88a2.52 2.52 0 0 1-3.5 0 2.47 2.47 0 0 1 0-3.5l3.88-3.88a1 1 0 0 0-1.42-1.42l-3.88 3.89a4.48 4.48 0 0 0 6.33 6.33l3.89-3.88a1 1 0 1 0-1.42-1.42Zm8.58-12.08a4.49 4.49 0 0 0-6.33 0l-3.89 3.88a1 1 0 0 0 1.42 1.42l3.88-3.88a2.52 2.52 0 0 1 3.5 0 2.47 2.47 0 0 1 0 3.5l-3.88 3.88a1 1 0 1 0 1.42 1.42l3.88-3.89a4.49 4.49 0 0 0 0-6.33ZM8.83 15.17a1 1 0 0 0 1.1.22 1 1 0 0 0 .32-.22l4.92-4.92a1 1 0 0 0-1.42-1.42l-4.92 4.92a1 1 0 0 0 0 1.42Z',
})
)
);

const createSROnlyLabel = (text: string) => {
const t = useTranslationsForLang('en');
return h(
'span',
{ 'is:raw': true, class: 'sr-only' },
`${t('a11y.sectionLink')} ${escape(text)}`
);
};

/**
* Configuration for the `rehype-autolink-headings` plugin.
* This set-up was informed by https://amberwilson.co.uk/blog/are-your-anchor-links-accessible/
*/
const autolinkConfig: AutolinkOptions = {
properties: { class: 'anchor-link' },
behavior: 'after',
group: ({ tagName }) => h('div', { tabIndex: -1, class: `heading-wrapper level-${tagName}` }),
content: (heading) => [AnchorLinkIcon, createSROnlyLabel(toString(heading))],
};

/**
* Rehype plugin to translate the headings' anchors according to the currently selected language.
*/
function rehypei18nAutolinkHeadings() {
const pageSourceDir = nodeResolve('./src/content/docs');
const baseUrl = 'https://docs.astro.build/';

const transformer: Transformer<Root> = (tree, file) => {
const pageUrl = mdFilePathToUrl(file.path, pageSourceDir, baseUrl);
const pageLang = getLanguageCodeFromPathname(pageUrl.pathname);
const englishText = useTranslationsForLang('en')('a11y.sectionLink');

// Find anchor links
visit(tree, 'element', (node) => {
if (node.tagName === 'a' && node.properties?.class === 'anchor-link') {
// Find a11y text labels
visit(node, 'text', (text) => {
const heading = text.value.replace(englishText!, '');
const t = useTranslationsForLang(pageLang as UILanguageKeys);
const title = t('a11y.sectionLink') || englishText;

text.value = title + heading;
});
}
});
};

return function attacher() {
return transformer;
};
}

/**
* Configure heading anchor links.
* Spread this into Astro’s `markdown.rehypePlugins` option.
*/
export const rehypeAutolink = (): RehypePlugins => [
// @ts-expect-error — Astro is not happy with this [plugin, options] tuple for some reason, but it works!
[rehypeAutolinkHeadings, autolinkConfig],
rehypei18nAutolinkHeadings(),
];
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 64 additions & 0 deletions src/components/starlight/MarkdownContent.astro
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,68 @@ const { entry } = Astro.props;
.sl-markdown-content :global(details + details > summary:not(.not-content *)) {
border-top: 0;
}

/* Custom styles for heading anchor links. */
.sl-markdown-content :global(.heading-wrapper) {
--icon-size: 0.75em;
--icon-spacing: 0.25em;
line-height: var(--sl-line-height-headings);
}

/* Set font-size on wrapper element, so line-height, margins etc. match heading size. */
.sl-markdown-content :global(.level-h2) {
font-size: var(--sl-text-h2);
}
.sl-markdown-content :global(.level-h3) {
font-size: var(--sl-text-h3);
}
.sl-markdown-content :global(.level-h4) {
font-size: var(--sl-text-h4);
}
.sl-markdown-content :global(.level-h5) {
font-size: var(--sl-text-h5);
}

.sl-markdown-content :global(.heading-wrapper > :first-child) {
margin-inline-end: calc(var(--icon-size) + var(--icon-spacing));
display: inline;
}

.sl-markdown-content :global(.anchor-link) {
margin-inline-start: calc(-1 * (var(--icon-size)));
color: var(--sl-color-gray-3);
}
.sl-markdown-content :global(.anchor-link:hover),
.sl-markdown-content :global(.anchor-link:focus) {
color: var(--sl-color-text-accent);
}
.sl-markdown-content :global(.heading-wrapper svg) {
display: inline;
width: var(--icon-size);
}

@media (hover: hover) {
.sl-markdown-content :global(.anchor-link) {
opacity: 0;
}
}
.sl-markdown-content :global(.heading-wrapper:hover > .anchor-link),
.sl-markdown-content :global(.anchor-link:focus) {
opacity: 1;
}

/* Float anchor links to the left of headings on larger screens. */
@media (min-width: 95em) {
.sl-markdown-content :global(.heading-wrapper) {
display: flex;
flex-direction: row-reverse;
justify-content: flex-end;
gap: var(--icon-spacing);
margin-inline-start: calc(-1 * (var(--icon-size) + var(--icon-spacing)));
}
.sl-markdown-content :global(.heading-wrapper > :first-child),
.sl-markdown-content :global(.anchor-link) {
margin: 0;
}
}
</style>

0 comments on commit 2429811

Please sign in to comment.